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 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub bridges: Option<BridgeInfo>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PluginInfo {
65 pub name: String,
67
68 pub version: String,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub description: Option<String>,
74
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub authors: Vec<String>,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub license: Option<String>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub repository: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct PlatformInfo {
94 pub variants: HashMap<String, VariantInfo>,
97}
98
99impl PlatformInfo {
100 pub fn new(library: String, checksum: String) -> Self {
102 let mut variants = HashMap::new();
103 variants.insert(
104 "release".to_string(),
105 VariantInfo {
106 library,
107 checksum,
108 build: None,
109 },
110 );
111 Self { variants }
112 }
113
114 #[must_use]
116 pub fn release(&self) -> Option<&VariantInfo> {
117 self.variants.get("release")
118 }
119
120 #[must_use]
122 pub fn variant(&self, name: &str) -> Option<&VariantInfo> {
123 self.variants.get(name)
124 }
125
126 #[must_use]
128 pub fn default_variant(&self) -> Option<&VariantInfo> {
129 self.release()
130 }
131
132 #[must_use]
134 pub fn variant_names(&self) -> Vec<&str> {
135 self.variants.keys().map(String::as_str).collect()
136 }
137
138 #[must_use]
140 pub fn has_variant(&self, name: &str) -> bool {
141 self.variants.contains_key(name)
142 }
143
144 pub fn add_variant(&mut self, name: String, info: VariantInfo) {
146 self.variants.insert(name, info);
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct VariantInfo {
156 pub library: String,
159
160 pub checksum: String,
163
164 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub build: Option<serde_json::Value>,
173}
174
175#[derive(Debug, Clone, Default, Serialize, Deserialize)]
180pub struct BuildInfo {
181 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub built_by: Option<String>,
184
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub built_at: Option<String>,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub host: Option<String>,
192
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub compiler: Option<String>,
196
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub rustbridge_version: Option<String>,
200
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub git: Option<GitInfo>,
204
205 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub custom: Option<HashMap<String, String>>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct GitInfo {
217 pub commit: String,
219
220 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub branch: Option<String>,
223
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub tag: Option<String>,
227
228 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub dirty: Option<bool>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct Sbom {
239 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub cyclonedx: Option<String>,
242
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub spdx: Option<String>,
246}
247
248fn is_valid_variant_name(name: &str) -> bool {
253 if name.is_empty() {
254 return false;
255 }
256
257 let chars: Vec<char> = name.chars().collect();
259 if !chars[0].is_ascii_lowercase() && !chars[0].is_ascii_digit() {
260 return false;
261 }
262 if !chars[chars.len() - 1].is_ascii_lowercase() && !chars[chars.len() - 1].is_ascii_digit() {
263 return false;
264 }
265
266 name.chars()
268 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct SchemaInfo {
274 pub path: String,
276
277 pub format: String,
279
280 pub checksum: String,
282
283 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub description: Option<String>,
286}
287
288#[derive(Debug, Clone, Default, Serialize, Deserialize)]
293pub struct BridgeInfo {
294 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
297 pub jni: HashMap<String, PlatformInfo>,
298}
299
300impl Manifest {
301 #[must_use]
303 pub fn new(name: &str, version: &str) -> Self {
304 Self {
305 bundle_version: BUNDLE_VERSION.to_string(),
306 plugin: PluginInfo {
307 name: name.to_string(),
308 version: version.to_string(),
309 description: None,
310 authors: Vec::new(),
311 license: None,
312 repository: None,
313 },
314 platforms: HashMap::new(),
315 build_info: None,
316 sbom: None,
317 schema_checksum: None,
318 notices: None,
319 license_file: None,
320 public_key: None,
321 schemas: HashMap::new(),
322 bridges: None,
323 }
324 }
325
326 pub fn add_platform(&mut self, platform: Platform, library_path: &str, checksum: &str) {
331 let platform_key = platform.as_str().to_string();
332
333 if let Some(platform_info) = self.platforms.get_mut(&platform_key) {
334 platform_info.variants.insert(
336 "release".to_string(),
337 VariantInfo {
338 library: library_path.to_string(),
339 checksum: format!("sha256:{checksum}"),
340 build: None,
341 },
342 );
343 } else {
344 self.platforms.insert(
346 platform_key,
347 PlatformInfo::new(library_path.to_string(), format!("sha256:{checksum}")),
348 );
349 }
350 }
351
352 pub fn add_platform_variant(
356 &mut self,
357 platform: Platform,
358 variant: &str,
359 library_path: &str,
360 checksum: &str,
361 build: Option<serde_json::Value>,
362 ) {
363 let platform_key = platform.as_str().to_string();
364
365 let platform_info = self
366 .platforms
367 .entry(platform_key)
368 .or_insert_with(|| PlatformInfo {
369 variants: HashMap::new(),
370 });
371
372 platform_info.variants.insert(
373 variant.to_string(),
374 VariantInfo {
375 library: library_path.to_string(),
376 checksum: format!("sha256:{checksum}"),
377 build,
378 },
379 );
380 }
381
382 pub fn set_public_key(&mut self, public_key: String) {
384 self.public_key = Some(public_key);
385 }
386
387 pub fn add_schema(
389 &mut self,
390 name: String,
391 path: String,
392 format: String,
393 checksum: String,
394 description: Option<String>,
395 ) {
396 self.schemas.insert(
397 name,
398 SchemaInfo {
399 path,
400 format,
401 checksum,
402 description,
403 },
404 );
405 }
406
407 pub fn set_build_info(&mut self, build_info: BuildInfo) {
409 self.build_info = Some(build_info);
410 }
411
412 #[must_use]
414 pub fn get_build_info(&self) -> Option<&BuildInfo> {
415 self.build_info.as_ref()
416 }
417
418 pub fn set_sbom(&mut self, sbom: Sbom) {
420 self.sbom = Some(sbom);
421 }
422
423 #[must_use]
425 pub fn get_sbom(&self) -> Option<&Sbom> {
426 self.sbom.as_ref()
427 }
428
429 pub fn set_schema_checksum(&mut self, checksum: String) {
431 self.schema_checksum = Some(checksum);
432 }
433
434 #[must_use]
436 pub fn get_schema_checksum(&self) -> Option<&str> {
437 self.schema_checksum.as_deref()
438 }
439
440 pub fn set_notices(&mut self, path: String) {
442 self.notices = Some(path);
443 }
444
445 #[must_use]
447 pub fn get_notices(&self) -> Option<&str> {
448 self.notices.as_deref()
449 }
450
451 pub fn set_license_file(&mut self, path: String) {
453 self.license_file = Some(path);
454 }
455
456 #[must_use]
458 pub fn get_license_file(&self) -> Option<&str> {
459 self.license_file.as_deref()
460 }
461
462 pub fn add_jni_bridge(
467 &mut self,
468 platform: Platform,
469 variant: &str,
470 library_path: &str,
471 checksum: &str,
472 ) {
473 let bridges = self.bridges.get_or_insert_with(BridgeInfo::default);
474 let platform_key = platform.as_str().to_string();
475
476 let platform_info = bridges
477 .jni
478 .entry(platform_key)
479 .or_insert_with(|| PlatformInfo {
480 variants: HashMap::new(),
481 });
482
483 platform_info.variants.insert(
484 variant.to_string(),
485 VariantInfo {
486 library: library_path.to_string(),
487 checksum: format!("sha256:{checksum}"),
488 build: None,
489 },
490 );
491 }
492
493 #[must_use]
495 pub fn has_jni_bridge(&self) -> bool {
496 self.bridges.as_ref().is_some_and(|b| !b.jni.is_empty())
497 }
498
499 #[must_use]
501 pub fn get_jni_bridge(&self, platform: Platform) -> Option<&PlatformInfo> {
502 self.bridges
503 .as_ref()
504 .and_then(|b| b.jni.get(platform.as_str()))
505 }
506
507 #[must_use]
511 pub fn get_variant(&self, platform: Platform, variant: Option<&str>) -> Option<&VariantInfo> {
512 let platform_info = self.platforms.get(platform.as_str())?;
513 let variant_name = variant.unwrap_or("release");
514 platform_info.variants.get(variant_name)
515 }
516
517 #[must_use]
519 pub fn get_release_variant(&self, platform: Platform) -> Option<&VariantInfo> {
520 self.get_variant(platform, Some("release"))
521 }
522
523 #[must_use]
525 pub fn list_variants(&self, platform: Platform) -> Vec<&str> {
526 self.platforms
527 .get(platform.as_str())
528 .map(|p| p.variant_names())
529 .unwrap_or_default()
530 }
531
532 #[must_use]
534 pub fn get_platform(&self, platform: Platform) -> Option<&PlatformInfo> {
535 self.platforms.get(platform.as_str())
536 }
537
538 #[must_use]
540 pub fn supports_platform(&self, platform: Platform) -> bool {
541 self.platforms.contains_key(platform.as_str())
542 }
543
544 #[must_use]
546 pub fn supported_platforms(&self) -> Vec<Platform> {
547 self.platforms
548 .keys()
549 .filter_map(|k| Platform::parse(k))
550 .collect()
551 }
552
553 pub fn validate(&self) -> BundleResult<()> {
555 if self.bundle_version.is_empty() {
557 return Err(BundleError::InvalidManifest(
558 "bundle_version is required".to_string(),
559 ));
560 }
561
562 if self.plugin.name.is_empty() {
564 return Err(BundleError::InvalidManifest(
565 "plugin.name is required".to_string(),
566 ));
567 }
568
569 if self.plugin.version.is_empty() {
571 return Err(BundleError::InvalidManifest(
572 "plugin.version is required".to_string(),
573 ));
574 }
575
576 if self.platforms.is_empty() {
578 return Err(BundleError::InvalidManifest(
579 "at least one platform must be defined".to_string(),
580 ));
581 }
582
583 for (key, info) in &self.platforms {
585 if Platform::parse(key).is_none() {
586 return Err(BundleError::InvalidManifest(format!(
587 "unknown platform: {key}"
588 )));
589 }
590
591 if info.variants.is_empty() {
593 return Err(BundleError::InvalidManifest(format!(
594 "platform {key}: at least one variant is required"
595 )));
596 }
597
598 if !info.variants.contains_key("release") {
600 return Err(BundleError::InvalidManifest(format!(
601 "platform {key}: 'release' variant is required"
602 )));
603 }
604
605 for (variant_name, variant_info) in &info.variants {
607 if !is_valid_variant_name(variant_name) {
609 return Err(BundleError::InvalidManifest(format!(
610 "platform {key}: invalid variant name '{variant_name}' \
611 (must be lowercase alphanumeric with hyphens)"
612 )));
613 }
614
615 if variant_info.library.is_empty() {
616 return Err(BundleError::InvalidManifest(format!(
617 "platform {key}, variant {variant_name}: library path is required"
618 )));
619 }
620
621 if variant_info.checksum.is_empty() {
622 return Err(BundleError::InvalidManifest(format!(
623 "platform {key}, variant {variant_name}: checksum is required"
624 )));
625 }
626
627 if !variant_info.checksum.starts_with("sha256:") {
628 return Err(BundleError::InvalidManifest(format!(
629 "platform {key}, variant {variant_name}: checksum must start with 'sha256:'"
630 )));
631 }
632 }
633 }
634
635 Ok(())
636 }
637
638 pub fn to_json(&self) -> BundleResult<String> {
640 Ok(serde_json::to_string_pretty(self)?)
641 }
642
643 pub fn from_json(json: &str) -> BundleResult<Self> {
645 Ok(serde_json::from_str(json)?)
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 #![allow(non_snake_case)]
652
653 use super::*;
654
655 #[test]
656 fn Manifest___new___creates_valid_minimal_manifest() {
657 let manifest = Manifest::new("test-plugin", "1.0.0");
658
659 assert_eq!(manifest.plugin.name, "test-plugin");
660 assert_eq!(manifest.plugin.version, "1.0.0");
661 assert_eq!(manifest.bundle_version, BUNDLE_VERSION);
662 assert!(manifest.platforms.is_empty());
663 }
664
665 #[test]
666 fn Manifest___add_platform___adds_platform_info() {
667 let mut manifest = Manifest::new("test-plugin", "1.0.0");
668 manifest.add_platform(
669 Platform::LinuxX86_64,
670 "lib/linux-x86_64/libtest.so",
671 "abc123",
672 );
673
674 assert!(manifest.supports_platform(Platform::LinuxX86_64));
675 assert!(!manifest.supports_platform(Platform::WindowsX86_64));
676
677 let info = manifest.get_platform(Platform::LinuxX86_64).unwrap();
678 let release = info.release().unwrap();
679 assert_eq!(release.library, "lib/linux-x86_64/libtest.so");
680 assert_eq!(release.checksum, "sha256:abc123");
681 }
682
683 #[test]
684 fn Manifest___add_platform___overwrites_existing() {
685 let mut manifest = Manifest::new("test", "1.0.0");
686 manifest.add_platform(Platform::LinuxX86_64, "lib/old.so", "old");
687 manifest.add_platform(Platform::LinuxX86_64, "lib/new.so", "new");
688
689 let info = manifest.get_platform(Platform::LinuxX86_64).unwrap();
690 let release = info.release().unwrap();
691 assert_eq!(release.library, "lib/new.so");
692 assert_eq!(release.checksum, "sha256:new");
693 }
694
695 #[test]
696 fn Manifest___validate___rejects_empty_name() {
697 let manifest = Manifest::new("", "1.0.0");
698 let result = manifest.validate();
699
700 assert!(result.is_err());
701 assert!(result.unwrap_err().to_string().contains("plugin.name"));
702 }
703
704 #[test]
705 fn Manifest___validate___rejects_empty_version() {
706 let manifest = Manifest::new("test", "");
707 let result = manifest.validate();
708
709 assert!(result.is_err());
710 assert!(result.unwrap_err().to_string().contains("plugin.version"));
711 }
712
713 #[test]
714 fn Manifest___validate___rejects_empty_platforms() {
715 let manifest = Manifest::new("test", "1.0.0");
716 let result = manifest.validate();
717
718 assert!(result.is_err());
719 assert!(
720 result
721 .unwrap_err()
722 .to_string()
723 .contains("at least one platform")
724 );
725 }
726
727 #[test]
728 fn Manifest___validate___rejects_invalid_checksum_format() {
729 let mut manifest = Manifest::new("test", "1.0.0");
730 let mut variants = HashMap::new();
732 variants.insert(
733 "release".to_string(),
734 VariantInfo {
735 library: "lib/test.so".to_string(),
736 checksum: "abc123".to_string(), build: None,
738 },
739 );
740 manifest
741 .platforms
742 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
743
744 let result = manifest.validate();
745
746 assert!(result.is_err());
747 assert!(result.unwrap_err().to_string().contains("sha256:"));
748 }
749
750 #[test]
751 fn Manifest___validate___rejects_unknown_platform() {
752 let mut manifest = Manifest::new("test", "1.0.0");
753 manifest.platforms.insert(
755 "invalid-platform".to_string(),
756 PlatformInfo::new("lib/test.so".to_string(), "sha256:abc123".to_string()),
757 );
758
759 let result = manifest.validate();
760
761 assert!(result.is_err());
762 assert!(result.unwrap_err().to_string().contains("unknown platform"));
763 }
764
765 #[test]
766 fn Manifest___validate___rejects_empty_library_path() {
767 let mut manifest = Manifest::new("test", "1.0.0");
768 let mut variants = HashMap::new();
769 variants.insert(
770 "release".to_string(),
771 VariantInfo {
772 library: "".to_string(),
773 checksum: "sha256:abc123".to_string(),
774 build: None,
775 },
776 );
777 manifest
778 .platforms
779 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
780
781 let result = manifest.validate();
782
783 assert!(result.is_err());
784 assert!(
785 result
786 .unwrap_err()
787 .to_string()
788 .contains("library path is required")
789 );
790 }
791
792 #[test]
793 fn Manifest___validate___rejects_empty_checksum() {
794 let mut manifest = Manifest::new("test", "1.0.0");
795 let mut variants = HashMap::new();
796 variants.insert(
797 "release".to_string(),
798 VariantInfo {
799 library: "lib/test.so".to_string(),
800 checksum: "".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("checksum is required")
816 );
817 }
818
819 #[test]
820 fn Manifest___validate___rejects_missing_release_variant() {
821 let mut manifest = Manifest::new("test", "1.0.0");
822 let mut variants = HashMap::new();
823 variants.insert(
824 "debug".to_string(), VariantInfo {
826 library: "lib/test.so".to_string(),
827 checksum: "sha256:abc123".to_string(),
828 build: None,
829 },
830 );
831 manifest
832 .platforms
833 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
834
835 let result = manifest.validate();
836
837 assert!(result.is_err());
838 assert!(
839 result
840 .unwrap_err()
841 .to_string()
842 .contains("'release' variant is required")
843 );
844 }
845
846 #[test]
847 fn Manifest___validate___rejects_invalid_variant_name() {
848 let mut manifest = Manifest::new("test", "1.0.0");
849 let mut variants = HashMap::new();
850 variants.insert(
851 "release".to_string(),
852 VariantInfo {
853 library: "lib/test.so".to_string(),
854 checksum: "sha256:abc123".to_string(),
855 build: None,
856 },
857 );
858 variants.insert(
859 "INVALID".to_string(), VariantInfo {
861 library: "lib/test.so".to_string(),
862 checksum: "sha256:abc123".to_string(),
863 build: None,
864 },
865 );
866 manifest
867 .platforms
868 .insert("linux-x86_64".to_string(), PlatformInfo { variants });
869
870 let result = manifest.validate();
871
872 assert!(result.is_err());
873 assert!(
874 result
875 .unwrap_err()
876 .to_string()
877 .contains("invalid variant name")
878 );
879 }
880
881 #[test]
882 fn Manifest___validate___accepts_valid_manifest() {
883 let mut manifest = Manifest::new("test-plugin", "1.0.0");
884 manifest.add_platform(
885 Platform::LinuxX86_64,
886 "lib/linux-x86_64/libtest.so",
887 "abc123",
888 );
889
890 assert!(manifest.validate().is_ok());
891 }
892
893 #[test]
894 fn Manifest___validate___accepts_all_platforms() {
895 let mut manifest = Manifest::new("all-platforms", "1.0.0");
896 for platform in Platform::all() {
897 manifest.add_platform(
898 *platform,
899 &format!("lib/{}/libtest", platform.as_str()),
900 "hash",
901 );
902 }
903
904 assert!(manifest.validate().is_ok());
905 assert_eq!(manifest.supported_platforms().len(), 6);
906 }
907
908 #[test]
909 fn Manifest___json_roundtrip___preserves_data() {
910 let mut manifest = Manifest::new("test-plugin", "1.0.0");
911 manifest.plugin.description = Some("A test plugin".to_string());
912 manifest.add_platform(
913 Platform::LinuxX86_64,
914 "lib/linux-x86_64/libtest.so",
915 "abc123",
916 );
917 manifest.add_platform(
918 Platform::DarwinAarch64,
919 "lib/darwin-aarch64/libtest.dylib",
920 "def456",
921 );
922
923 let json = manifest.to_json().unwrap();
924 let parsed = Manifest::from_json(&json).unwrap();
925
926 assert_eq!(parsed.plugin.name, manifest.plugin.name);
927 assert_eq!(parsed.plugin.version, manifest.plugin.version);
928 assert_eq!(parsed.plugin.description, manifest.plugin.description);
929 assert_eq!(parsed.platforms.len(), 2);
930 }
931
932 #[test]
933 fn Manifest___json_roundtrip___preserves_all_plugin_fields() {
934 let mut manifest = Manifest::new("full-plugin", "2.3.4");
935 manifest.plugin.description = Some("Full description".to_string());
936 manifest.plugin.authors = vec!["Author 1".to_string(), "Author 2".to_string()];
937 manifest.plugin.license = Some("Apache-2.0".to_string());
938 manifest.plugin.repository = Some("https://github.com/test/repo".to_string());
939 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
940
941 let json = manifest.to_json().unwrap();
942 let parsed = Manifest::from_json(&json).unwrap();
943
944 assert_eq!(parsed.plugin.description, manifest.plugin.description);
945 assert_eq!(parsed.plugin.authors, manifest.plugin.authors);
946 assert_eq!(parsed.plugin.license, manifest.plugin.license);
947 assert_eq!(parsed.plugin.repository, manifest.plugin.repository);
948 }
949
950 #[test]
951 fn Manifest___json_roundtrip___preserves_schemas() {
952 let mut manifest = Manifest::new("schema-plugin", "1.0.0");
953 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
954 manifest.add_schema(
955 "messages.h".to_string(),
956 "schema/messages.h".to_string(),
957 "c-header".to_string(),
958 "sha256:abc".to_string(),
959 Some("C header for binary transport".to_string()),
960 );
961
962 let json = manifest.to_json().unwrap();
963 let parsed = Manifest::from_json(&json).unwrap();
964
965 assert_eq!(parsed.schemas.len(), 1);
966 let schema = parsed.schemas.get("messages.h").unwrap();
967 assert_eq!(schema.path, "schema/messages.h");
968 assert_eq!(schema.format, "c-header");
969 assert_eq!(schema.checksum, "sha256:abc");
970 assert_eq!(
971 schema.description,
972 Some("C header for binary transport".to_string())
973 );
974 }
975
976 #[test]
977 fn Manifest___json_roundtrip___preserves_public_key() {
978 let mut manifest = Manifest::new("signed-plugin", "1.0.0");
979 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
980 manifest.set_public_key("RWSxxxxxxxxxxxxxxxx".to_string());
981
982 let json = manifest.to_json().unwrap();
983 let parsed = Manifest::from_json(&json).unwrap();
984
985 assert_eq!(parsed.public_key, Some("RWSxxxxxxxxxxxxxxxx".to_string()));
986 }
987
988 #[test]
989 fn Manifest___from_json___invalid_json___returns_error() {
990 let result = Manifest::from_json("{ invalid }");
991
992 assert!(result.is_err());
993 }
994
995 #[test]
996 fn Manifest___from_json___missing_required_fields___returns_error() {
997 let result = Manifest::from_json(r#"{"bundle_version": "1.0"}"#);
998
999 assert!(result.is_err());
1000 }
1001
1002 #[test]
1003 fn Manifest___supported_platforms___returns_all_platforms() {
1004 let mut manifest = Manifest::new("test", "1.0.0");
1005 manifest.add_platform(Platform::LinuxX86_64, "lib/a.so", "a");
1006 manifest.add_platform(Platform::DarwinAarch64, "lib/b.dylib", "b");
1007
1008 let platforms = manifest.supported_platforms();
1009 assert_eq!(platforms.len(), 2);
1010 assert!(platforms.contains(&Platform::LinuxX86_64));
1011 assert!(platforms.contains(&Platform::DarwinAarch64));
1012 }
1013
1014 #[test]
1015 fn Manifest___get_platform___returns_none_for_unsupported() {
1016 let mut manifest = Manifest::new("test", "1.0.0");
1017 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1018
1019 assert!(manifest.get_platform(Platform::LinuxX86_64).is_some());
1020 assert!(manifest.get_platform(Platform::WindowsX86_64).is_none());
1021 }
1022
1023 #[test]
1024 fn BuildInfo___default___all_fields_none() {
1025 let build_info = BuildInfo::default();
1026
1027 assert!(build_info.built_by.is_none());
1028 assert!(build_info.built_at.is_none());
1029 assert!(build_info.host.is_none());
1030 assert!(build_info.compiler.is_none());
1031 assert!(build_info.rustbridge_version.is_none());
1032 assert!(build_info.git.is_none());
1033 }
1034
1035 #[test]
1036 fn Manifest___build_info___roundtrip() {
1037 let mut manifest = Manifest::new("test", "1.0.0");
1038 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1039 manifest.set_build_info(BuildInfo {
1040 built_by: Some("GitHub Actions".to_string()),
1041 built_at: Some("2025-01-26T10:30:00Z".to_string()),
1042 host: Some("x86_64-unknown-linux-gnu".to_string()),
1043 compiler: Some("rustc 1.90.0".to_string()),
1044 rustbridge_version: Some("0.2.0".to_string()),
1045 git: Some(GitInfo {
1046 commit: "abc123".to_string(),
1047 branch: Some("main".to_string()),
1048 tag: Some("v1.0.0".to_string()),
1049 dirty: Some(false),
1050 }),
1051 custom: None,
1052 });
1053
1054 let json = manifest.to_json().unwrap();
1055 let parsed = Manifest::from_json(&json).unwrap();
1056
1057 let build_info = parsed.get_build_info().unwrap();
1058 assert_eq!(build_info.built_by, Some("GitHub Actions".to_string()));
1059 assert_eq!(build_info.compiler, Some("rustc 1.90.0".to_string()));
1060
1061 let git = build_info.git.as_ref().unwrap();
1062 assert_eq!(git.commit, "abc123");
1063 assert_eq!(git.branch, Some("main".to_string()));
1064 assert_eq!(git.dirty, Some(false));
1065 }
1066
1067 #[test]
1068 fn Manifest___sbom___roundtrip() {
1069 let mut manifest = Manifest::new("test", "1.0.0");
1070 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1071 manifest.set_sbom(Sbom {
1072 cyclonedx: Some("sbom/sbom.cdx.json".to_string()),
1073 spdx: Some("sbom/sbom.spdx.json".to_string()),
1074 });
1075
1076 let json = manifest.to_json().unwrap();
1077 let parsed = Manifest::from_json(&json).unwrap();
1078
1079 let sbom = parsed.get_sbom().unwrap();
1080 assert_eq!(sbom.cyclonedx, Some("sbom/sbom.cdx.json".to_string()));
1081 assert_eq!(sbom.spdx, Some("sbom/sbom.spdx.json".to_string()));
1082 }
1083
1084 #[test]
1085 fn Manifest___variants___roundtrip() {
1086 let mut manifest = Manifest::new("test", "1.0.0");
1087 manifest.add_platform_variant(
1088 Platform::LinuxX86_64,
1089 "release",
1090 "lib/linux-x86_64/release/libtest.so",
1091 "hash1",
1092 Some(serde_json::json!({
1093 "profile": "release",
1094 "opt_level": "3"
1095 })),
1096 );
1097 manifest.add_platform_variant(
1098 Platform::LinuxX86_64,
1099 "debug",
1100 "lib/linux-x86_64/debug/libtest.so",
1101 "hash2",
1102 Some(serde_json::json!({
1103 "profile": "debug",
1104 "opt_level": "0"
1105 })),
1106 );
1107
1108 let json = manifest.to_json().unwrap();
1109 let parsed = Manifest::from_json(&json).unwrap();
1110
1111 let variants = parsed.list_variants(Platform::LinuxX86_64);
1112 assert_eq!(variants.len(), 2);
1113 assert!(variants.contains(&"release"));
1114 assert!(variants.contains(&"debug"));
1115
1116 let release = parsed
1117 .get_variant(Platform::LinuxX86_64, Some("release"))
1118 .unwrap();
1119 assert_eq!(release.library, "lib/linux-x86_64/release/libtest.so");
1120 assert_eq!(
1121 release.build.as_ref().unwrap()["profile"],
1122 serde_json::json!("release")
1123 );
1124 }
1125
1126 #[test]
1127 fn Manifest___schema_checksum___roundtrip() {
1128 let mut manifest = Manifest::new("test", "1.0.0");
1129 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1130 manifest.set_schema_checksum("sha256:abcdef123456".to_string());
1131
1132 let json = manifest.to_json().unwrap();
1133 let parsed = Manifest::from_json(&json).unwrap();
1134
1135 assert_eq!(parsed.get_schema_checksum(), Some("sha256:abcdef123456"));
1136 }
1137
1138 #[test]
1139 fn Manifest___notices___roundtrip() {
1140 let mut manifest = Manifest::new("test", "1.0.0");
1141 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1142 manifest.set_notices("docs/NOTICES.txt".to_string());
1143
1144 let json = manifest.to_json().unwrap();
1145 let parsed = Manifest::from_json(&json).unwrap();
1146
1147 assert_eq!(parsed.get_notices(), Some("docs/NOTICES.txt"));
1148 }
1149
1150 #[test]
1151 fn Manifest___license_file___roundtrip() {
1152 let mut manifest = Manifest::new("test", "1.0.0");
1153 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1154 manifest.set_license_file("legal/LICENSE".to_string());
1155
1156 let json = manifest.to_json().unwrap();
1157 let parsed = Manifest::from_json(&json).unwrap();
1158
1159 assert_eq!(parsed.get_license_file(), Some("legal/LICENSE"));
1160 }
1161
1162 #[test]
1163 fn is_valid_variant_name___accepts_valid_names() {
1164 assert!(is_valid_variant_name("release"));
1165 assert!(is_valid_variant_name("debug"));
1166 assert!(is_valid_variant_name("nightly"));
1167 assert!(is_valid_variant_name("opt-size"));
1168 assert!(is_valid_variant_name("v1"));
1169 assert!(is_valid_variant_name("build123"));
1170 }
1171
1172 #[test]
1173 fn is_valid_variant_name___rejects_invalid_names() {
1174 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")); }
1182
1183 #[test]
1184 fn PlatformInfo___new___creates_release_variant() {
1185 let platform_info =
1186 PlatformInfo::new("lib/test.so".to_string(), "sha256:abc123".to_string());
1187
1188 assert!(platform_info.has_variant("release"));
1189 let release = platform_info.release().unwrap();
1190 assert_eq!(release.library, "lib/test.so");
1191 assert_eq!(release.checksum, "sha256:abc123");
1192 }
1193
1194 #[test]
1195 fn PlatformInfo___variant_names___returns_all_variants() {
1196 let mut platform_info =
1197 PlatformInfo::new("lib/release.so".to_string(), "sha256:abc".to_string());
1198 platform_info.add_variant(
1199 "debug".to_string(),
1200 VariantInfo {
1201 library: "lib/debug.so".to_string(),
1202 checksum: "sha256:def".to_string(),
1203 build: None,
1204 },
1205 );
1206
1207 let names = platform_info.variant_names();
1208 assert_eq!(names.len(), 2);
1209 assert!(names.contains(&"release"));
1210 assert!(names.contains(&"debug"));
1211 }
1212
1213 #[test]
1214 fn Manifest___has_jni_bridge___returns_false_when_no_bridges() {
1215 let manifest = Manifest::new("test", "1.0.0");
1216
1217 assert!(!manifest.has_jni_bridge());
1218 }
1219
1220 #[test]
1221 fn Manifest___add_jni_bridge___adds_bridge_info() {
1222 let mut manifest = Manifest::new("test", "1.0.0");
1223 manifest.add_jni_bridge(
1224 Platform::LinuxX86_64,
1225 "release",
1226 "bridge/jni/linux-x86_64/release/librustbridge_jni.so",
1227 "abc123",
1228 );
1229
1230 assert!(manifest.has_jni_bridge());
1231
1232 let bridge = manifest.get_jni_bridge(Platform::LinuxX86_64).unwrap();
1233 let release = bridge.release().unwrap();
1234 assert_eq!(
1235 release.library,
1236 "bridge/jni/linux-x86_64/release/librustbridge_jni.so"
1237 );
1238 assert_eq!(release.checksum, "sha256:abc123");
1239 }
1240
1241 #[test]
1242 fn Manifest___add_jni_bridge___multiple_platforms() {
1243 let mut manifest = Manifest::new("test", "1.0.0");
1244 manifest.add_jni_bridge(
1245 Platform::LinuxX86_64,
1246 "release",
1247 "bridge/jni/linux-x86_64/release/librustbridge_jni.so",
1248 "abc123",
1249 );
1250 manifest.add_jni_bridge(
1251 Platform::DarwinAarch64,
1252 "release",
1253 "bridge/jni/darwin-aarch64/release/librustbridge_jni.dylib",
1254 "def456",
1255 );
1256
1257 assert!(manifest.get_jni_bridge(Platform::LinuxX86_64).is_some());
1258 assert!(manifest.get_jni_bridge(Platform::DarwinAarch64).is_some());
1259 assert!(manifest.get_jni_bridge(Platform::WindowsX86_64).is_none());
1260 }
1261
1262 #[test]
1263 fn Manifest___jni_bridge___json_roundtrip() {
1264 let mut manifest = Manifest::new("test", "1.0.0");
1265 manifest.add_platform(Platform::LinuxX86_64, "lib/test.so", "hash");
1266 manifest.add_jni_bridge(
1267 Platform::LinuxX86_64,
1268 "release",
1269 "bridge/jni/linux-x86_64/release/librustbridge_jni.so",
1270 "abc123",
1271 );
1272
1273 let json = manifest.to_json().unwrap();
1274 let parsed = Manifest::from_json(&json).unwrap();
1275
1276 assert!(parsed.has_jni_bridge());
1277 let bridge = parsed.get_jni_bridge(Platform::LinuxX86_64).unwrap();
1278 let release = bridge.release().unwrap();
1279 assert_eq!(
1280 release.library,
1281 "bridge/jni/linux-x86_64/release/librustbridge_jni.so"
1282 );
1283 }
1284
1285 #[test]
1286 fn BridgeInfo___default___empty_jni_map() {
1287 let bridge_info = BridgeInfo::default();
1288
1289 assert!(bridge_info.jni.is_empty());
1290 }
1291}