1use crate::{BundleError, BundleResult, MANIFEST_FILE, Manifest, Platform};
6use minisign::SecretKey;
7use sha2::{Digest, Sha256};
8use std::fs::{self, File};
9use std::io::Write;
10use std::path::Path;
11use zip::ZipWriter;
12use zip::write::SimpleFileOptions;
13
14#[derive(Debug)]
30pub struct BundleBuilder {
31 manifest: Manifest,
32 files: Vec<BundleFile>,
33 signing_key: Option<(String, SecretKey)>, }
35
36#[derive(Debug)]
38struct BundleFile {
39 archive_path: String,
41 contents: Vec<u8>,
43}
44
45impl BundleBuilder {
46 #[must_use]
48 pub fn new(manifest: Manifest) -> Self {
49 Self {
50 manifest,
51 files: Vec::new(),
52 signing_key: None,
53 }
54 }
55
56 pub fn with_signing_key(mut self, public_key_base64: String, secret_key: SecretKey) -> Self {
65 self.manifest.set_public_key(public_key_base64.clone());
66 self.signing_key = Some((public_key_base64, secret_key));
67 self
68 }
69
70 pub fn add_library<P: AsRef<Path>>(
78 self,
79 platform: Platform,
80 library_path: P,
81 ) -> BundleResult<Self> {
82 self.add_library_variant(platform, "release", library_path)
83 }
84
85 pub fn add_library_variant<P: AsRef<Path>>(
95 mut self,
96 platform: Platform,
97 variant: &str,
98 library_path: P,
99 ) -> BundleResult<Self> {
100 let library_path = library_path.as_ref();
101
102 let contents = fs::read(library_path).map_err(|e| {
104 BundleError::LibraryNotFound(format!("{}: {}", library_path.display(), e))
105 })?;
106
107 let checksum = compute_sha256(&contents);
109
110 let file_name = library_path
112 .file_name()
113 .ok_or_else(|| {
114 BundleError::InvalidManifest(format!(
115 "Invalid library path: {}",
116 library_path.display()
117 ))
118 })?
119 .to_string_lossy();
120 let archive_path = format!("lib/{}/{}/{}", platform.as_str(), variant, file_name);
121
122 self.manifest
124 .add_platform_variant(platform, variant, &archive_path, &checksum, None);
125
126 self.files.push(BundleFile {
128 archive_path,
129 contents,
130 });
131
132 Ok(self)
133 }
134
135 pub fn add_library_variant_with_build<P: AsRef<Path>>(
140 mut self,
141 platform: Platform,
142 variant: &str,
143 library_path: P,
144 build: serde_json::Value,
145 ) -> BundleResult<Self> {
146 let library_path = library_path.as_ref();
147
148 let contents = fs::read(library_path).map_err(|e| {
150 BundleError::LibraryNotFound(format!("{}: {}", library_path.display(), e))
151 })?;
152
153 let checksum = compute_sha256(&contents);
155
156 let file_name = library_path
158 .file_name()
159 .ok_or_else(|| {
160 BundleError::InvalidManifest(format!(
161 "Invalid library path: {}",
162 library_path.display()
163 ))
164 })?
165 .to_string_lossy();
166 let archive_path = format!("lib/{}/{}/{}", platform.as_str(), variant, file_name);
167
168 self.manifest.add_platform_variant(
170 platform,
171 variant,
172 &archive_path,
173 &checksum,
174 Some(build),
175 );
176
177 self.files.push(BundleFile {
179 archive_path,
180 contents,
181 });
182
183 Ok(self)
184 }
185
186 pub fn add_jni_library<P: AsRef<Path>>(
194 self,
195 platform: Platform,
196 library_path: P,
197 ) -> BundleResult<Self> {
198 self.add_jni_library_variant(platform, "release", library_path)
199 }
200
201 pub fn add_jni_library_variant<P: AsRef<Path>>(
211 mut self,
212 platform: Platform,
213 variant: &str,
214 library_path: P,
215 ) -> BundleResult<Self> {
216 let library_path = library_path.as_ref();
217
218 let contents = fs::read(library_path).map_err(|e| {
220 BundleError::LibraryNotFound(format!("{}: {}", library_path.display(), e))
221 })?;
222
223 let checksum = compute_sha256(&contents);
225
226 let file_name = library_path
228 .file_name()
229 .ok_or_else(|| {
230 BundleError::InvalidManifest(format!(
231 "Invalid library path: {}",
232 library_path.display()
233 ))
234 })?
235 .to_string_lossy();
236 let archive_path = format!("bridge/jni/{}/{}/{}", platform.as_str(), variant, file_name);
237
238 self.manifest
240 .add_jni_bridge(platform, variant, &archive_path, &checksum);
241
242 self.files.push(BundleFile {
244 archive_path,
245 contents,
246 });
247
248 Ok(self)
249 }
250
251 pub fn add_schema_file<P: AsRef<Path>>(
260 mut self,
261 source_path: P,
262 archive_name: &str,
263 ) -> BundleResult<Self> {
264 let source_path = source_path.as_ref();
265
266 let contents = fs::read(source_path).map_err(|e| {
267 BundleError::Io(std::io::Error::new(
268 e.kind(),
269 format!(
270 "Failed to read schema file {}: {}",
271 source_path.display(),
272 e
273 ),
274 ))
275 })?;
276
277 let checksum = compute_sha256(&contents);
279
280 let format = detect_schema_format(archive_name);
282
283 let archive_path = format!("schema/{archive_name}");
284
285 self.manifest.add_schema(
287 archive_name.to_string(),
288 archive_path.clone(),
289 format,
290 checksum,
291 None, );
293
294 self.files.push(BundleFile {
295 archive_path,
296 contents,
297 });
298
299 Ok(self)
300 }
301
302 pub fn add_doc_file<P: AsRef<Path>>(
306 mut self,
307 source_path: P,
308 archive_name: &str,
309 ) -> BundleResult<Self> {
310 let source_path = source_path.as_ref();
311
312 let contents = fs::read(source_path).map_err(|e| {
313 BundleError::Io(std::io::Error::new(
314 e.kind(),
315 format!("Failed to read doc file {}: {}", source_path.display(), e),
316 ))
317 })?;
318
319 let archive_path = format!("docs/{archive_name}");
320
321 self.files.push(BundleFile {
322 archive_path,
323 contents,
324 });
325
326 Ok(self)
327 }
328
329 pub fn add_bytes(mut self, archive_path: &str, contents: Vec<u8>) -> Self {
331 self.files.push(BundleFile {
332 archive_path: archive_path.to_string(),
333 contents,
334 });
335 self
336 }
337
338 pub fn with_build_info(mut self, build_info: crate::BuildInfo) -> Self {
340 self.manifest.set_build_info(build_info);
341 self
342 }
343
344 pub fn with_sbom(mut self, sbom: crate::Sbom) -> Self {
346 self.manifest.set_sbom(sbom);
347 self
348 }
349
350 pub fn add_notices_file<P: AsRef<Path>>(mut self, source_path: P) -> BundleResult<Self> {
354 let source_path = source_path.as_ref();
355
356 let contents = fs::read(source_path).map_err(|e| {
357 BundleError::Io(std::io::Error::new(
358 e.kind(),
359 format!(
360 "Failed to read notices file {}: {}",
361 source_path.display(),
362 e
363 ),
364 ))
365 })?;
366
367 let archive_path = "docs/NOTICES.txt".to_string();
368 self.manifest.set_notices(archive_path.clone());
369
370 self.files.push(BundleFile {
371 archive_path,
372 contents,
373 });
374
375 Ok(self)
376 }
377
378 pub fn add_license_file<P: AsRef<Path>>(mut self, source_path: P) -> BundleResult<Self> {
383 let source_path = source_path.as_ref();
384
385 let contents = fs::read(source_path).map_err(|e| {
386 BundleError::Io(std::io::Error::new(
387 e.kind(),
388 format!(
389 "Failed to read license file {}: {}",
390 source_path.display(),
391 e
392 ),
393 ))
394 })?;
395
396 let archive_path = "legal/LICENSE".to_string();
397 self.manifest.set_license_file(archive_path.clone());
398
399 self.files.push(BundleFile {
400 archive_path,
401 contents,
402 });
403
404 Ok(self)
405 }
406
407 pub fn add_sbom_file<P: AsRef<Path>>(
411 mut self,
412 source_path: P,
413 archive_name: &str,
414 ) -> BundleResult<Self> {
415 let source_path = source_path.as_ref();
416
417 let contents = fs::read(source_path).map_err(|e| {
418 BundleError::Io(std::io::Error::new(
419 e.kind(),
420 format!("Failed to read SBOM file {}: {}", source_path.display(), e),
421 ))
422 })?;
423
424 let archive_path = format!("sbom/{archive_name}");
425
426 self.files.push(BundleFile {
427 archive_path,
428 contents,
429 });
430
431 Ok(self)
432 }
433
434 pub fn write<P: AsRef<Path>>(self, output_path: P) -> BundleResult<()> {
436 let output_path = output_path.as_ref();
437
438 self.manifest.validate()?;
440
441 let file = File::create(output_path)?;
443 let mut zip = ZipWriter::new(file);
444 let options =
445 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
446
447 let manifest_json = self.manifest.to_json()?;
449 zip.start_file(MANIFEST_FILE, options)?;
450 zip.write_all(manifest_json.as_bytes())?;
451
452 if let Some((ref _public_key, ref secret_key)) = self.signing_key {
454 let signature = sign_data(secret_key, manifest_json.as_bytes())?;
455 zip.start_file(format!("{MANIFEST_FILE}.minisig"), options)?;
456 zip.write_all(signature.as_bytes())?;
457 }
458
459 for bundle_file in &self.files {
461 zip.start_file(&bundle_file.archive_path, options)?;
462 zip.write_all(&bundle_file.contents)?;
463
464 if let Some((ref _public_key, ref secret_key)) = self.signing_key {
466 if bundle_file.archive_path.starts_with("lib/")
468 || bundle_file.archive_path.starts_with("bridge/")
469 {
470 let signature = sign_data(secret_key, &bundle_file.contents)?;
471 let sig_path = format!("{}.minisig", bundle_file.archive_path);
472 zip.start_file(&sig_path, options)?;
473 zip.write_all(signature.as_bytes())?;
474 }
475 }
476 }
477
478 zip.finish()?;
479
480 Ok(())
481 }
482
483 #[must_use]
485 pub fn manifest(&self) -> &Manifest {
486 &self.manifest
487 }
488
489 pub fn manifest_mut(&mut self) -> &mut Manifest {
491 &mut self.manifest
492 }
493}
494
495pub fn compute_sha256(data: &[u8]) -> String {
497 let mut hasher = Sha256::new();
498 hasher.update(data);
499 let result = hasher.finalize();
500 hex::encode(result)
501}
502
503pub fn verify_sha256(data: &[u8], expected: &str) -> bool {
505 let actual = compute_sha256(data);
506
507 let expected_hex = expected.strip_prefix("sha256:").unwrap_or(expected);
509
510 actual == expected_hex
511}
512
513fn detect_schema_format(filename: &str) -> String {
515 if filename.ends_with(".h") || filename.ends_with(".hpp") {
516 "c-header".to_string()
517 } else if filename.ends_with(".json") {
518 "json-schema".to_string()
519 } else {
520 "unknown".to_string()
521 }
522}
523
524fn sign_data(secret_key: &SecretKey, data: &[u8]) -> BundleResult<String> {
528 let signature_box = minisign::sign(
529 None, secret_key, data, None, None, )
533 .map_err(|e| BundleError::Io(std::io::Error::other(format!("Failed to sign data: {e}"))))?;
534
535 Ok(signature_box.to_string())
536}
537
538#[cfg(test)]
539mod tests {
540 #![allow(non_snake_case)]
541
542 use super::*;
543 use tempfile::TempDir;
544
545 #[test]
546 fn compute_sha256___returns_consistent_hash() {
547 let data = b"hello world";
548 let hash1 = compute_sha256(data);
549 let hash2 = compute_sha256(data);
550
551 assert_eq!(hash1, hash2);
552 assert_eq!(hash1.len(), 64); }
554
555 #[test]
556 fn compute_sha256___different_data___different_hash() {
557 let hash1 = compute_sha256(b"hello");
558 let hash2 = compute_sha256(b"world");
559
560 assert_ne!(hash1, hash2);
561 }
562
563 #[test]
564 fn compute_sha256___empty_data___returns_valid_hash() {
565 let hash = compute_sha256(b"");
566
567 assert_eq!(hash.len(), 64);
568 assert_eq!(
570 hash,
571 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
572 );
573 }
574
575 #[test]
576 fn verify_sha256___accepts_valid_checksum() {
577 let data = b"hello world";
578 let checksum = compute_sha256(data);
579
580 assert!(verify_sha256(data, &checksum));
581 assert!(verify_sha256(data, &format!("sha256:{checksum}")));
582 }
583
584 #[test]
585 fn verify_sha256___rejects_invalid_checksum() {
586 let data = b"hello world";
587
588 assert!(!verify_sha256(data, "invalid"));
589 assert!(!verify_sha256(data, "sha256:invalid"));
590 }
591
592 #[test]
593 fn verify_sha256___case_sensitive___rejects_uppercase() {
594 let data = b"hello world";
595 let checksum = compute_sha256(data).to_uppercase();
596
597 assert!(!verify_sha256(data, &checksum));
598 }
599
600 #[test]
601 fn BundleBuilder___add_bytes___adds_file() {
602 let manifest = Manifest::new("test", "1.0.0");
603 let builder = BundleBuilder::new(manifest).add_bytes("test.txt", b"hello".to_vec());
604
605 assert_eq!(builder.files.len(), 1);
606 assert_eq!(builder.files[0].archive_path, "test.txt");
607 assert_eq!(builder.files[0].contents, b"hello");
608 }
609
610 #[test]
611 fn BundleBuilder___add_bytes___multiple_files() {
612 let manifest = Manifest::new("test", "1.0.0");
613 let builder = BundleBuilder::new(manifest)
614 .add_bytes("file1.txt", b"content1".to_vec())
615 .add_bytes("file2.txt", b"content2".to_vec())
616 .add_bytes("dir/file3.txt", b"content3".to_vec());
617
618 assert_eq!(builder.files.len(), 3);
619 }
620
621 #[test]
622 fn BundleBuilder___add_library___nonexistent_file___returns_error() {
623 let manifest = Manifest::new("test", "1.0.0");
624 let result = BundleBuilder::new(manifest)
625 .add_library(Platform::LinuxX86_64, "/nonexistent/path/libtest.so");
626
627 assert!(result.is_err());
628 let err = result.unwrap_err();
629 assert!(matches!(err, BundleError::LibraryNotFound(_)));
630 assert!(err.to_string().contains("/nonexistent/path/libtest.so"));
631 }
632
633 #[test]
634 fn BundleBuilder___add_library___valid_file___computes_checksum() {
635 let temp_dir = TempDir::new().unwrap();
636 let lib_path = temp_dir.path().join("libtest.so");
637 fs::write(&lib_path, b"fake library").unwrap();
638
639 let manifest = Manifest::new("test", "1.0.0");
640 let builder = BundleBuilder::new(manifest)
641 .add_library(Platform::LinuxX86_64, &lib_path)
642 .unwrap();
643
644 let platform_info = builder
645 .manifest
646 .get_platform(Platform::LinuxX86_64)
647 .unwrap();
648 let release = platform_info.release().unwrap();
649 assert!(release.checksum.starts_with("sha256:"));
650 assert_eq!(release.library, "lib/linux-x86_64/release/libtest.so");
651 }
652
653 #[test]
654 fn BundleBuilder___add_library_variant___adds_multiple_variants() {
655 let temp_dir = TempDir::new().unwrap();
656 let release_lib = temp_dir.path().join("libtest_release.so");
657 let debug_lib = temp_dir.path().join("libtest_debug.so");
658 fs::write(&release_lib, b"release library").unwrap();
659 fs::write(&debug_lib, b"debug library").unwrap();
660
661 let manifest = Manifest::new("test", "1.0.0");
662 let builder = BundleBuilder::new(manifest)
663 .add_library_variant(Platform::LinuxX86_64, "release", &release_lib)
664 .unwrap()
665 .add_library_variant(Platform::LinuxX86_64, "debug", &debug_lib)
666 .unwrap();
667
668 let platform_info = builder
669 .manifest
670 .get_platform(Platform::LinuxX86_64)
671 .unwrap();
672
673 assert!(platform_info.has_variant("release"));
674 assert!(platform_info.has_variant("debug"));
675
676 let release = platform_info.variant("release").unwrap();
677 let debug = platform_info.variant("debug").unwrap();
678
679 assert_eq!(
680 release.library,
681 "lib/linux-x86_64/release/libtest_release.so"
682 );
683 assert_eq!(debug.library, "lib/linux-x86_64/debug/libtest_debug.so");
684 }
685
686 #[test]
687 fn BundleBuilder___add_library___multiple_platforms() {
688 let temp_dir = TempDir::new().unwrap();
689
690 let linux_lib = temp_dir.path().join("libtest.so");
691 let macos_lib = temp_dir.path().join("libtest.dylib");
692 fs::write(&linux_lib, b"linux lib").unwrap();
693 fs::write(&macos_lib, b"macos lib").unwrap();
694
695 let manifest = Manifest::new("test", "1.0.0");
696 let builder = BundleBuilder::new(manifest)
697 .add_library(Platform::LinuxX86_64, &linux_lib)
698 .unwrap()
699 .add_library(Platform::DarwinAarch64, &macos_lib)
700 .unwrap();
701
702 assert!(builder.manifest.supports_platform(Platform::LinuxX86_64));
703 assert!(builder.manifest.supports_platform(Platform::DarwinAarch64));
704 assert!(!builder.manifest.supports_platform(Platform::WindowsX86_64));
705 }
706
707 #[test]
708 fn BundleBuilder___add_schema_file___nonexistent___returns_error() {
709 let manifest = Manifest::new("test", "1.0.0");
710 let result =
711 BundleBuilder::new(manifest).add_schema_file("/nonexistent/schema.h", "schema.h");
712
713 assert!(result.is_err());
714 }
715
716 #[test]
717 fn BundleBuilder___add_schema_file___detects_c_header_format() {
718 let temp_dir = TempDir::new().unwrap();
719 let schema_path = temp_dir.path().join("messages.h");
720 fs::write(&schema_path, b"#include <stdint.h>").unwrap();
721
722 let manifest = Manifest::new("test", "1.0.0");
723 let builder = BundleBuilder::new(manifest)
724 .add_schema_file(&schema_path, "messages.h")
725 .unwrap();
726
727 let schema_info = builder.manifest.schemas.get("messages.h").unwrap();
728 assert_eq!(schema_info.format, "c-header");
729 }
730
731 #[test]
732 fn BundleBuilder___add_schema_file___detects_json_schema_format() {
733 let temp_dir = TempDir::new().unwrap();
734 let schema_path = temp_dir.path().join("schema.json");
735 fs::write(&schema_path, b"{}").unwrap();
736
737 let manifest = Manifest::new("test", "1.0.0");
738 let builder = BundleBuilder::new(manifest)
739 .add_schema_file(&schema_path, "schema.json")
740 .unwrap();
741
742 let schema_info = builder.manifest.schemas.get("schema.json").unwrap();
743 assert_eq!(schema_info.format, "json-schema");
744 }
745
746 #[test]
747 fn BundleBuilder___add_schema_file___unknown_format() {
748 let temp_dir = TempDir::new().unwrap();
749 let schema_path = temp_dir.path().join("schema.xyz");
750 fs::write(&schema_path, b"content").unwrap();
751
752 let manifest = Manifest::new("test", "1.0.0");
753 let builder = BundleBuilder::new(manifest)
754 .add_schema_file(&schema_path, "schema.xyz")
755 .unwrap();
756
757 let schema_info = builder.manifest.schemas.get("schema.xyz").unwrap();
758 assert_eq!(schema_info.format, "unknown");
759 }
760
761 #[test]
762 fn BundleBuilder___write___invalid_manifest___returns_error() {
763 let temp_dir = TempDir::new().unwrap();
764 let output_path = temp_dir.path().join("test.rbp");
765
766 let manifest = Manifest::new("test", "1.0.0");
768 let result = BundleBuilder::new(manifest).write(&output_path);
769
770 assert!(result.is_err());
771 let err = result.unwrap_err();
772 assert!(matches!(err, BundleError::InvalidManifest(_)));
773 }
774
775 #[test]
776 fn BundleBuilder___write___creates_valid_bundle() {
777 let temp_dir = TempDir::new().unwrap();
778 let lib_path = temp_dir.path().join("libtest.so");
779 let output_path = temp_dir.path().join("test.rbp");
780 fs::write(&lib_path, b"fake library").unwrap();
781
782 let manifest = Manifest::new("test", "1.0.0");
783 BundleBuilder::new(manifest)
784 .add_library(Platform::LinuxX86_64, &lib_path)
785 .unwrap()
786 .write(&output_path)
787 .unwrap();
788
789 assert!(output_path.exists());
790
791 let file = File::open(&output_path).unwrap();
793 let archive = zip::ZipArchive::new(file).unwrap();
794 assert!(archive.len() >= 2); }
796
797 #[test]
798 fn BundleBuilder___manifest_mut___allows_modification() {
799 let manifest = Manifest::new("test", "1.0.0");
800 let mut builder = BundleBuilder::new(manifest);
801
802 builder.manifest_mut().plugin.description = Some("Modified".to_string());
803
804 assert_eq!(
805 builder.manifest().plugin.description,
806 Some("Modified".to_string())
807 );
808 }
809
810 #[test]
811 fn detect_schema_format___hpp_extension___returns_c_header() {
812 assert_eq!(detect_schema_format("types.hpp"), "c-header");
813 }
814
815 #[test]
823 fn sign_data___generates_verifiable_signature() {
824 use minisign::{KeyPair, PublicKey};
825 use std::io::Cursor;
826
827 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
829
830 let test_data = b"Hello, rustbridge!";
832
833 let mut reader = Cursor::new(test_data.as_slice());
835 let signature_box = minisign::sign(
836 Some(&keypair.pk),
837 &keypair.sk,
838 &mut reader,
839 Some("trusted comment"),
840 Some("untrusted comment"),
841 )
842 .unwrap();
843
844 let pk = PublicKey::from_base64(&keypair.pk.to_base64()).unwrap();
846 let mut verify_reader = Cursor::new(test_data.as_slice());
847 let result = minisign::verify(&pk, &signature_box, &mut verify_reader, true, false, false);
848
849 assert!(result.is_ok(), "Signature verification should succeed");
850 }
851
852 #[test]
853 fn sign_data___wrong_data___verification_fails() {
854 use minisign::{KeyPair, PublicKey};
855 use std::io::Cursor;
856
857 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
858 let test_data = b"Hello, rustbridge!";
859 let wrong_data = b"Hello, rustbridge?"; let mut reader = Cursor::new(test_data.as_slice());
863 let signature_box =
864 minisign::sign(Some(&keypair.pk), &keypair.sk, &mut reader, None, None).unwrap();
865
866 let pk = PublicKey::from_base64(&keypair.pk.to_base64()).unwrap();
868 let mut verify_reader = Cursor::new(wrong_data.as_slice());
869 let result = minisign::verify(&pk, &signature_box, &mut verify_reader, true, false, false);
870
871 assert!(result.is_err(), "Verification should fail with wrong data");
872 }
873
874 #[test]
875 fn sign_data___signature_format___has_prehash_algorithm_id() {
876 use base64::Engine;
877 use minisign::KeyPair;
878 use std::io::Cursor;
879
880 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
881 let test_data = b"test";
882
883 let mut reader = Cursor::new(test_data.as_slice());
884 let signature_box = minisign::sign(
885 Some(&keypair.pk),
886 &keypair.sk,
887 &mut reader,
888 Some("trusted"),
889 Some("untrusted"),
890 )
891 .unwrap();
892
893 let sig_string = signature_box.into_string();
894 let lines: Vec<&str> = sig_string.lines().collect();
895
896 let sig_base64 = lines[1];
898 let sig_bytes = base64::engine::general_purpose::STANDARD
899 .decode(sig_base64)
900 .unwrap();
901
902 assert_eq!(sig_bytes[0], 0x45, "First byte should be 'E'");
904 assert_eq!(
905 sig_bytes[1], 0x44,
906 "Second byte should be 'D' for prehashed"
907 );
908 assert_eq!(sig_bytes.len(), 74, "Signature should be 74 bytes");
909 }
910
911 #[test]
912 fn sign_data___public_key_format___has_ed_algorithm_id() {
913 use base64::Engine;
914 use minisign::KeyPair;
915
916 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
917 let pk_base64 = keypair.pk.to_base64();
918 let pk_bytes = base64::engine::general_purpose::STANDARD
919 .decode(&pk_base64)
920 .unwrap();
921
922 assert_eq!(pk_bytes[0], 0x45, "First byte should be 'E'");
924 assert_eq!(
925 pk_bytes[1], 0x64,
926 "Second byte should be 'd' for public key"
927 );
928 assert_eq!(pk_bytes.len(), 42, "Public key should be 42 bytes");
929 }
930
931 #[test]
932 fn BundleBuilder___add_license_file___adds_file_to_legal_dir() {
933 let temp_dir = TempDir::new().unwrap();
934 let license_path = temp_dir.path().join("LICENSE");
935 fs::write(&license_path, b"MIT License\n\nCopyright...").unwrap();
936
937 let manifest = Manifest::new("test", "1.0.0");
938 let builder = BundleBuilder::new(manifest)
939 .add_license_file(&license_path)
940 .unwrap();
941
942 assert_eq!(builder.files.len(), 1);
944 assert_eq!(builder.files[0].archive_path, "legal/LICENSE");
945
946 assert_eq!(builder.manifest.get_license_file(), Some("legal/LICENSE"));
948 }
949
950 #[test]
951 fn BundleBuilder___add_license_file___nonexistent___returns_error() {
952 let manifest = Manifest::new("test", "1.0.0");
953 let result = BundleBuilder::new(manifest).add_license_file("/nonexistent/LICENSE");
954
955 assert!(result.is_err());
956 }
957
958 #[test]
959 fn BundleBuilder___add_jni_library___adds_file_to_bridge_dir() {
960 let temp_dir = TempDir::new().unwrap();
961 let jni_lib = temp_dir.path().join("librustbridge_jni.so");
962 fs::write(&jni_lib, b"fake jni library").unwrap();
963
964 let manifest = Manifest::new("test", "1.0.0");
965 let builder = BundleBuilder::new(manifest)
966 .add_jni_library(Platform::LinuxX86_64, &jni_lib)
967 .unwrap();
968
969 assert_eq!(builder.files.len(), 1);
971 assert_eq!(
972 builder.files[0].archive_path,
973 "bridge/jni/linux-x86_64/release/librustbridge_jni.so"
974 );
975
976 assert!(builder.manifest.has_jni_bridge());
978 let bridge = builder
979 .manifest
980 .get_jni_bridge(Platform::LinuxX86_64)
981 .unwrap();
982 let release = bridge.release().unwrap();
983 assert_eq!(
984 release.library,
985 "bridge/jni/linux-x86_64/release/librustbridge_jni.so"
986 );
987 assert!(release.checksum.starts_with("sha256:"));
988 }
989
990 #[test]
991 fn BundleBuilder___add_jni_library_variant___adds_multiple_variants() {
992 let temp_dir = TempDir::new().unwrap();
993 let release_lib = temp_dir.path().join("librustbridge_jni_release.so");
994 let debug_lib = temp_dir.path().join("librustbridge_jni_debug.so");
995 fs::write(&release_lib, b"release jni library").unwrap();
996 fs::write(&debug_lib, b"debug jni library").unwrap();
997
998 let manifest = Manifest::new("test", "1.0.0");
999 let builder = BundleBuilder::new(manifest)
1000 .add_jni_library_variant(Platform::LinuxX86_64, "release", &release_lib)
1001 .unwrap()
1002 .add_jni_library_variant(Platform::LinuxX86_64, "debug", &debug_lib)
1003 .unwrap();
1004
1005 assert_eq!(builder.files.len(), 2);
1006
1007 let bridge = builder
1008 .manifest
1009 .get_jni_bridge(Platform::LinuxX86_64)
1010 .unwrap();
1011 assert!(bridge.has_variant("release"));
1012 assert!(bridge.has_variant("debug"));
1013 }
1014
1015 #[test]
1016 fn BundleBuilder___add_jni_library___nonexistent___returns_error() {
1017 let manifest = Manifest::new("test", "1.0.0");
1018 let result = BundleBuilder::new(manifest)
1019 .add_jni_library(Platform::LinuxX86_64, "/nonexistent/librustbridge_jni.so");
1020
1021 assert!(result.is_err());
1022 let err = result.unwrap_err();
1023 assert!(matches!(err, BundleError::LibraryNotFound(_)));
1024 }
1025
1026 #[test]
1027 fn BundleBuilder___write___with_jni_library___creates_valid_bundle() {
1028 let temp_dir = TempDir::new().unwrap();
1029
1030 let plugin_lib = temp_dir.path().join("libtest.so");
1032 let jni_lib = temp_dir.path().join("librustbridge_jni.so");
1033 let output_path = temp_dir.path().join("test.rbp");
1034 fs::write(&plugin_lib, b"fake plugin library").unwrap();
1035 fs::write(&jni_lib, b"fake jni library").unwrap();
1036
1037 let manifest = Manifest::new("test", "1.0.0");
1038 BundleBuilder::new(manifest)
1039 .add_library(Platform::LinuxX86_64, &plugin_lib)
1040 .unwrap()
1041 .add_jni_library(Platform::LinuxX86_64, &jni_lib)
1042 .unwrap()
1043 .write(&output_path)
1044 .unwrap();
1045
1046 assert!(output_path.exists());
1047
1048 let file = File::open(&output_path).unwrap();
1050 let archive = zip::ZipArchive::new(file).unwrap();
1051
1052 let names: Vec<&str> = archive.file_names().collect();
1053 assert!(names.contains(&"manifest.json"));
1054 assert!(names.contains(&"lib/linux-x86_64/release/libtest.so"));
1055 assert!(names.contains(&"bridge/jni/linux-x86_64/release/librustbridge_jni.so"));
1056 }
1057}