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_schema_file<P: AsRef<Path>>(
195 mut self,
196 source_path: P,
197 archive_name: &str,
198 ) -> BundleResult<Self> {
199 let source_path = source_path.as_ref();
200
201 let contents = fs::read(source_path).map_err(|e| {
202 BundleError::Io(std::io::Error::new(
203 e.kind(),
204 format!(
205 "Failed to read schema file {}: {}",
206 source_path.display(),
207 e
208 ),
209 ))
210 })?;
211
212 let checksum = compute_sha256(&contents);
214
215 let format = detect_schema_format(archive_name);
217
218 let archive_path = format!("schema/{archive_name}");
219
220 self.manifest.add_schema(
222 archive_name.to_string(),
223 archive_path.clone(),
224 format,
225 checksum,
226 None, );
228
229 self.files.push(BundleFile {
230 archive_path,
231 contents,
232 });
233
234 Ok(self)
235 }
236
237 pub fn add_doc_file<P: AsRef<Path>>(
241 mut self,
242 source_path: P,
243 archive_name: &str,
244 ) -> BundleResult<Self> {
245 let source_path = source_path.as_ref();
246
247 let contents = fs::read(source_path).map_err(|e| {
248 BundleError::Io(std::io::Error::new(
249 e.kind(),
250 format!("Failed to read doc file {}: {}", source_path.display(), e),
251 ))
252 })?;
253
254 let archive_path = format!("docs/{archive_name}");
255
256 self.files.push(BundleFile {
257 archive_path,
258 contents,
259 });
260
261 Ok(self)
262 }
263
264 pub fn add_bytes(mut self, archive_path: &str, contents: Vec<u8>) -> Self {
266 self.files.push(BundleFile {
267 archive_path: archive_path.to_string(),
268 contents,
269 });
270 self
271 }
272
273 pub fn with_build_info(mut self, build_info: crate::BuildInfo) -> Self {
275 self.manifest.set_build_info(build_info);
276 self
277 }
278
279 pub fn with_sbom(mut self, sbom: crate::Sbom) -> Self {
281 self.manifest.set_sbom(sbom);
282 self
283 }
284
285 pub fn add_notices_file<P: AsRef<Path>>(mut self, source_path: P) -> BundleResult<Self> {
289 let source_path = source_path.as_ref();
290
291 let contents = fs::read(source_path).map_err(|e| {
292 BundleError::Io(std::io::Error::new(
293 e.kind(),
294 format!(
295 "Failed to read notices file {}: {}",
296 source_path.display(),
297 e
298 ),
299 ))
300 })?;
301
302 let archive_path = "docs/NOTICES.txt".to_string();
303 self.manifest.set_notices(archive_path.clone());
304
305 self.files.push(BundleFile {
306 archive_path,
307 contents,
308 });
309
310 Ok(self)
311 }
312
313 pub fn add_license_file<P: AsRef<Path>>(mut self, source_path: P) -> BundleResult<Self> {
318 let source_path = source_path.as_ref();
319
320 let contents = fs::read(source_path).map_err(|e| {
321 BundleError::Io(std::io::Error::new(
322 e.kind(),
323 format!(
324 "Failed to read license file {}: {}",
325 source_path.display(),
326 e
327 ),
328 ))
329 })?;
330
331 let archive_path = "legal/LICENSE".to_string();
332 self.manifest.set_license_file(archive_path.clone());
333
334 self.files.push(BundleFile {
335 archive_path,
336 contents,
337 });
338
339 Ok(self)
340 }
341
342 pub fn add_sbom_file<P: AsRef<Path>>(
346 mut self,
347 source_path: P,
348 archive_name: &str,
349 ) -> BundleResult<Self> {
350 let source_path = source_path.as_ref();
351
352 let contents = fs::read(source_path).map_err(|e| {
353 BundleError::Io(std::io::Error::new(
354 e.kind(),
355 format!("Failed to read SBOM file {}: {}", source_path.display(), e),
356 ))
357 })?;
358
359 let archive_path = format!("sbom/{archive_name}");
360
361 self.files.push(BundleFile {
362 archive_path,
363 contents,
364 });
365
366 Ok(self)
367 }
368
369 pub fn write<P: AsRef<Path>>(self, output_path: P) -> BundleResult<()> {
371 let output_path = output_path.as_ref();
372
373 self.manifest.validate()?;
375
376 let file = File::create(output_path)?;
378 let mut zip = ZipWriter::new(file);
379 let options =
380 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
381
382 let manifest_json = self.manifest.to_json()?;
384 zip.start_file(MANIFEST_FILE, options)?;
385 zip.write_all(manifest_json.as_bytes())?;
386
387 if let Some((ref _public_key, ref secret_key)) = self.signing_key {
389 let signature = sign_data(secret_key, manifest_json.as_bytes())?;
390 zip.start_file(format!("{MANIFEST_FILE}.minisig"), options)?;
391 zip.write_all(signature.as_bytes())?;
392 }
393
394 for bundle_file in &self.files {
396 zip.start_file(&bundle_file.archive_path, options)?;
397 zip.write_all(&bundle_file.contents)?;
398
399 if let Some((ref _public_key, ref secret_key)) = self.signing_key {
401 if bundle_file.archive_path.starts_with("lib/")
403 || bundle_file.archive_path.starts_with("bridge/")
404 {
405 let signature = sign_data(secret_key, &bundle_file.contents)?;
406 let sig_path = format!("{}.minisig", bundle_file.archive_path);
407 zip.start_file(&sig_path, options)?;
408 zip.write_all(signature.as_bytes())?;
409 }
410 }
411 }
412
413 zip.finish()?;
414
415 Ok(())
416 }
417
418 pub fn set_variant_build_info(
422 &mut self,
423 platform: Platform,
424 variant: &str,
425 build_info: crate::BuildInfo,
426 ) -> BundleResult<()> {
427 let variant_info = self.get_variant_mut(platform, variant)?;
428 variant_info.build_info = Some(build_info);
429 Ok(())
430 }
431
432 pub fn set_variant_sbom(
434 &mut self,
435 platform: Platform,
436 variant: &str,
437 sbom: crate::Sbom,
438 ) -> BundleResult<()> {
439 let variant_info = self.get_variant_mut(platform, variant)?;
440 variant_info.sbom = Some(sbom);
441 Ok(())
442 }
443
444 pub fn set_variant_schema_checksum(
446 &mut self,
447 platform: Platform,
448 variant: &str,
449 checksum: String,
450 ) -> BundleResult<()> {
451 let variant_info = self.get_variant_mut(platform, variant)?;
452 variant_info.schema_checksum = Some(checksum);
453 Ok(())
454 }
455
456 pub fn set_variant_schemas(
458 &mut self,
459 platform: Platform,
460 variant: &str,
461 schemas: std::collections::HashMap<String, crate::SchemaInfo>,
462 ) -> BundleResult<()> {
463 let variant_info = self.get_variant_mut(platform, variant)?;
464 variant_info.schemas = schemas;
465 Ok(())
466 }
467
468 fn get_variant_mut(
470 &mut self,
471 platform: Platform,
472 variant: &str,
473 ) -> BundleResult<&mut crate::VariantInfo> {
474 let platform_info = self
475 .manifest
476 .platforms
477 .get_mut(platform.as_str())
478 .ok_or_else(|| {
479 BundleError::UnsupportedPlatform(format!(
480 "Platform {} not in manifest",
481 platform.as_str()
482 ))
483 })?;
484 platform_info
485 .variants
486 .get_mut(variant)
487 .ok_or_else(|| BundleError::VariantNotFound {
488 platform: platform.as_str().to_string(),
489 variant: variant.to_string(),
490 })
491 }
492
493 #[must_use]
495 pub fn manifest(&self) -> &Manifest {
496 &self.manifest
497 }
498
499 pub fn manifest_mut(&mut self) -> &mut Manifest {
501 &mut self.manifest
502 }
503}
504
505pub fn compute_sha256(data: &[u8]) -> String {
507 let mut hasher = Sha256::new();
508 hasher.update(data);
509 let result = hasher.finalize();
510 hex::encode(result)
511}
512
513pub fn verify_sha256(data: &[u8], expected: &str) -> bool {
515 let actual = compute_sha256(data);
516
517 let expected_hex = expected.strip_prefix("sha256:").unwrap_or(expected);
519
520 actual == expected_hex
521}
522
523fn detect_schema_format(filename: &str) -> String {
525 if filename.ends_with(".h") || filename.ends_with(".hpp") {
526 "c-header".to_string()
527 } else if filename.ends_with(".json") {
528 "json-schema".to_string()
529 } else {
530 "unknown".to_string()
531 }
532}
533
534fn sign_data(secret_key: &SecretKey, data: &[u8]) -> BundleResult<String> {
538 let signature_box = minisign::sign(
539 None, secret_key, data, None, None, )
543 .map_err(|e| BundleError::Io(std::io::Error::other(format!("Failed to sign data: {e}"))))?;
544
545 Ok(signature_box.to_string())
546}
547
548#[cfg(test)]
549mod tests {
550 #![allow(non_snake_case)]
551
552 use super::*;
553 use tempfile::TempDir;
554
555 #[test]
556 fn compute_sha256___returns_consistent_hash() {
557 let data = b"hello world";
558 let hash1 = compute_sha256(data);
559 let hash2 = compute_sha256(data);
560
561 assert_eq!(hash1, hash2);
562 assert_eq!(hash1.len(), 64); }
564
565 #[test]
566 fn compute_sha256___different_data___different_hash() {
567 let hash1 = compute_sha256(b"hello");
568 let hash2 = compute_sha256(b"world");
569
570 assert_ne!(hash1, hash2);
571 }
572
573 #[test]
574 fn compute_sha256___empty_data___returns_valid_hash() {
575 let hash = compute_sha256(b"");
576
577 assert_eq!(hash.len(), 64);
578 assert_eq!(
580 hash,
581 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
582 );
583 }
584
585 #[test]
586 fn verify_sha256___accepts_valid_checksum() {
587 let data = b"hello world";
588 let checksum = compute_sha256(data);
589
590 assert!(verify_sha256(data, &checksum));
591 assert!(verify_sha256(data, &format!("sha256:{checksum}")));
592 }
593
594 #[test]
595 fn verify_sha256___rejects_invalid_checksum() {
596 let data = b"hello world";
597
598 assert!(!verify_sha256(data, "invalid"));
599 assert!(!verify_sha256(data, "sha256:invalid"));
600 }
601
602 #[test]
603 fn verify_sha256___case_sensitive___rejects_uppercase() {
604 let data = b"hello world";
605 let checksum = compute_sha256(data).to_uppercase();
606
607 assert!(!verify_sha256(data, &checksum));
608 }
609
610 #[test]
611 fn BundleBuilder___add_bytes___adds_file() {
612 let manifest = Manifest::new("test", "1.0.0");
613 let builder = BundleBuilder::new(manifest).add_bytes("test.txt", b"hello".to_vec());
614
615 assert_eq!(builder.files.len(), 1);
616 assert_eq!(builder.files[0].archive_path, "test.txt");
617 assert_eq!(builder.files[0].contents, b"hello");
618 }
619
620 #[test]
621 fn BundleBuilder___add_bytes___multiple_files() {
622 let manifest = Manifest::new("test", "1.0.0");
623 let builder = BundleBuilder::new(manifest)
624 .add_bytes("file1.txt", b"content1".to_vec())
625 .add_bytes("file2.txt", b"content2".to_vec())
626 .add_bytes("dir/file3.txt", b"content3".to_vec());
627
628 assert_eq!(builder.files.len(), 3);
629 }
630
631 #[test]
632 fn BundleBuilder___add_library___nonexistent_file___returns_error() {
633 let manifest = Manifest::new("test", "1.0.0");
634 let result = BundleBuilder::new(manifest)
635 .add_library(Platform::LinuxX86_64, "/nonexistent/path/libtest.so");
636
637 assert!(result.is_err());
638 let err = result.unwrap_err();
639 assert!(matches!(err, BundleError::LibraryNotFound(_)));
640 assert!(err.to_string().contains("/nonexistent/path/libtest.so"));
641 }
642
643 #[test]
644 fn BundleBuilder___add_library___valid_file___computes_checksum() {
645 let temp_dir = TempDir::new().unwrap();
646 let lib_path = temp_dir.path().join("libtest.so");
647 fs::write(&lib_path, b"fake library").unwrap();
648
649 let manifest = Manifest::new("test", "1.0.0");
650 let builder = BundleBuilder::new(manifest)
651 .add_library(Platform::LinuxX86_64, &lib_path)
652 .unwrap();
653
654 let platform_info = builder
655 .manifest
656 .get_platform(Platform::LinuxX86_64)
657 .unwrap();
658 let release = platform_info.release().unwrap();
659 assert!(release.checksum.starts_with("sha256:"));
660 assert_eq!(release.library, "lib/linux-x86_64/release/libtest.so");
661 }
662
663 #[test]
664 fn BundleBuilder___add_library_variant___adds_multiple_variants() {
665 let temp_dir = TempDir::new().unwrap();
666 let release_lib = temp_dir.path().join("libtest_release.so");
667 let debug_lib = temp_dir.path().join("libtest_debug.so");
668 fs::write(&release_lib, b"release library").unwrap();
669 fs::write(&debug_lib, b"debug library").unwrap();
670
671 let manifest = Manifest::new("test", "1.0.0");
672 let builder = BundleBuilder::new(manifest)
673 .add_library_variant(Platform::LinuxX86_64, "release", &release_lib)
674 .unwrap()
675 .add_library_variant(Platform::LinuxX86_64, "debug", &debug_lib)
676 .unwrap();
677
678 let platform_info = builder
679 .manifest
680 .get_platform(Platform::LinuxX86_64)
681 .unwrap();
682
683 assert!(platform_info.has_variant("release"));
684 assert!(platform_info.has_variant("debug"));
685
686 let release = platform_info.variant("release").unwrap();
687 let debug = platform_info.variant("debug").unwrap();
688
689 assert_eq!(
690 release.library,
691 "lib/linux-x86_64/release/libtest_release.so"
692 );
693 assert_eq!(debug.library, "lib/linux-x86_64/debug/libtest_debug.so");
694 }
695
696 #[test]
697 fn BundleBuilder___add_library___multiple_platforms() {
698 let temp_dir = TempDir::new().unwrap();
699
700 let linux_lib = temp_dir.path().join("libtest.so");
701 let macos_lib = temp_dir.path().join("libtest.dylib");
702 fs::write(&linux_lib, b"linux lib").unwrap();
703 fs::write(&macos_lib, b"macos lib").unwrap();
704
705 let manifest = Manifest::new("test", "1.0.0");
706 let builder = BundleBuilder::new(manifest)
707 .add_library(Platform::LinuxX86_64, &linux_lib)
708 .unwrap()
709 .add_library(Platform::DarwinAarch64, &macos_lib)
710 .unwrap();
711
712 assert!(builder.manifest.supports_platform(Platform::LinuxX86_64));
713 assert!(builder.manifest.supports_platform(Platform::DarwinAarch64));
714 assert!(!builder.manifest.supports_platform(Platform::WindowsX86_64));
715 }
716
717 #[test]
718 fn BundleBuilder___add_schema_file___nonexistent___returns_error() {
719 let manifest = Manifest::new("test", "1.0.0");
720 let result =
721 BundleBuilder::new(manifest).add_schema_file("/nonexistent/schema.h", "schema.h");
722
723 assert!(result.is_err());
724 }
725
726 #[test]
727 fn BundleBuilder___add_schema_file___detects_c_header_format() {
728 let temp_dir = TempDir::new().unwrap();
729 let schema_path = temp_dir.path().join("messages.h");
730 fs::write(&schema_path, b"#include <stdint.h>").unwrap();
731
732 let manifest = Manifest::new("test", "1.0.0");
733 let builder = BundleBuilder::new(manifest)
734 .add_schema_file(&schema_path, "messages.h")
735 .unwrap();
736
737 let schema_info = builder.manifest.schemas.get("messages.h").unwrap();
738 assert_eq!(schema_info.format, "c-header");
739 }
740
741 #[test]
742 fn BundleBuilder___add_schema_file___detects_json_schema_format() {
743 let temp_dir = TempDir::new().unwrap();
744 let schema_path = temp_dir.path().join("schema.json");
745 fs::write(&schema_path, b"{}").unwrap();
746
747 let manifest = Manifest::new("test", "1.0.0");
748 let builder = BundleBuilder::new(manifest)
749 .add_schema_file(&schema_path, "schema.json")
750 .unwrap();
751
752 let schema_info = builder.manifest.schemas.get("schema.json").unwrap();
753 assert_eq!(schema_info.format, "json-schema");
754 }
755
756 #[test]
757 fn BundleBuilder___add_schema_file___unknown_format() {
758 let temp_dir = TempDir::new().unwrap();
759 let schema_path = temp_dir.path().join("schema.xyz");
760 fs::write(&schema_path, b"content").unwrap();
761
762 let manifest = Manifest::new("test", "1.0.0");
763 let builder = BundleBuilder::new(manifest)
764 .add_schema_file(&schema_path, "schema.xyz")
765 .unwrap();
766
767 let schema_info = builder.manifest.schemas.get("schema.xyz").unwrap();
768 assert_eq!(schema_info.format, "unknown");
769 }
770
771 #[test]
772 fn BundleBuilder___write___invalid_manifest___returns_error() {
773 let temp_dir = TempDir::new().unwrap();
774 let output_path = temp_dir.path().join("test.rbp");
775
776 let manifest = Manifest::new("test", "1.0.0");
778 let result = BundleBuilder::new(manifest).write(&output_path);
779
780 assert!(result.is_err());
781 let err = result.unwrap_err();
782 assert!(matches!(err, BundleError::InvalidManifest(_)));
783 }
784
785 #[test]
786 fn BundleBuilder___write___creates_valid_bundle() {
787 let temp_dir = TempDir::new().unwrap();
788 let lib_path = temp_dir.path().join("libtest.so");
789 let output_path = temp_dir.path().join("test.rbp");
790 fs::write(&lib_path, b"fake library").unwrap();
791
792 let manifest = Manifest::new("test", "1.0.0");
793 BundleBuilder::new(manifest)
794 .add_library(Platform::LinuxX86_64, &lib_path)
795 .unwrap()
796 .write(&output_path)
797 .unwrap();
798
799 assert!(output_path.exists());
800
801 let file = File::open(&output_path).unwrap();
803 let archive = zip::ZipArchive::new(file).unwrap();
804 assert!(archive.len() >= 2); }
806
807 #[test]
808 fn BundleBuilder___manifest_mut___allows_modification() {
809 let manifest = Manifest::new("test", "1.0.0");
810 let mut builder = BundleBuilder::new(manifest);
811
812 builder.manifest_mut().plugin.description = Some("Modified".to_string());
813
814 assert_eq!(
815 builder.manifest().plugin.description,
816 Some("Modified".to_string())
817 );
818 }
819
820 #[test]
821 fn detect_schema_format___hpp_extension___returns_c_header() {
822 assert_eq!(detect_schema_format("types.hpp"), "c-header");
823 }
824
825 #[test]
833 fn sign_data___generates_verifiable_signature() {
834 use minisign::{KeyPair, PublicKey};
835 use std::io::Cursor;
836
837 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
839
840 let test_data = b"Hello, rustbridge!";
842
843 let mut reader = Cursor::new(test_data.as_slice());
845 let signature_box = minisign::sign(
846 Some(&keypair.pk),
847 &keypair.sk,
848 &mut reader,
849 Some("trusted comment"),
850 Some("untrusted comment"),
851 )
852 .unwrap();
853
854 let pk = PublicKey::from_base64(&keypair.pk.to_base64()).unwrap();
856 let mut verify_reader = Cursor::new(test_data.as_slice());
857 let result = minisign::verify(&pk, &signature_box, &mut verify_reader, true, false, false);
858
859 assert!(result.is_ok(), "Signature verification should succeed");
860 }
861
862 #[test]
863 fn sign_data___wrong_data___verification_fails() {
864 use minisign::{KeyPair, PublicKey};
865 use std::io::Cursor;
866
867 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
868 let test_data = b"Hello, rustbridge!";
869 let wrong_data = b"Hello, rustbridge?"; let mut reader = Cursor::new(test_data.as_slice());
873 let signature_box =
874 minisign::sign(Some(&keypair.pk), &keypair.sk, &mut reader, None, None).unwrap();
875
876 let pk = PublicKey::from_base64(&keypair.pk.to_base64()).unwrap();
878 let mut verify_reader = Cursor::new(wrong_data.as_slice());
879 let result = minisign::verify(&pk, &signature_box, &mut verify_reader, true, false, false);
880
881 assert!(result.is_err(), "Verification should fail with wrong data");
882 }
883
884 #[test]
885 fn sign_data___signature_format___has_prehash_algorithm_id() {
886 use base64::Engine;
887 use minisign::KeyPair;
888 use std::io::Cursor;
889
890 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
891 let test_data = b"test";
892
893 let mut reader = Cursor::new(test_data.as_slice());
894 let signature_box = minisign::sign(
895 Some(&keypair.pk),
896 &keypair.sk,
897 &mut reader,
898 Some("trusted"),
899 Some("untrusted"),
900 )
901 .unwrap();
902
903 let sig_string = signature_box.into_string();
904 let lines: Vec<&str> = sig_string.lines().collect();
905
906 let sig_base64 = lines[1];
908 let sig_bytes = base64::engine::general_purpose::STANDARD
909 .decode(sig_base64)
910 .unwrap();
911
912 assert_eq!(sig_bytes[0], 0x45, "First byte should be 'E'");
914 assert_eq!(
915 sig_bytes[1], 0x44,
916 "Second byte should be 'D' for prehashed"
917 );
918 assert_eq!(sig_bytes.len(), 74, "Signature should be 74 bytes");
919 }
920
921 #[test]
922 fn sign_data___public_key_format___has_ed_algorithm_id() {
923 use base64::Engine;
924 use minisign::KeyPair;
925
926 let keypair = KeyPair::generate_unencrypted_keypair().unwrap();
927 let pk_base64 = keypair.pk.to_base64();
928 let pk_bytes = base64::engine::general_purpose::STANDARD
929 .decode(&pk_base64)
930 .unwrap();
931
932 assert_eq!(pk_bytes[0], 0x45, "First byte should be 'E'");
934 assert_eq!(
935 pk_bytes[1], 0x64,
936 "Second byte should be 'd' for public key"
937 );
938 assert_eq!(pk_bytes.len(), 42, "Public key should be 42 bytes");
939 }
940
941 #[test]
942 fn BundleBuilder___add_license_file___adds_file_to_legal_dir() {
943 let temp_dir = TempDir::new().unwrap();
944 let license_path = temp_dir.path().join("LICENSE");
945 fs::write(&license_path, b"MIT License\n\nCopyright...").unwrap();
946
947 let manifest = Manifest::new("test", "1.0.0");
948 let builder = BundleBuilder::new(manifest)
949 .add_license_file(&license_path)
950 .unwrap();
951
952 assert_eq!(builder.files.len(), 1);
954 assert_eq!(builder.files[0].archive_path, "legal/LICENSE");
955
956 assert_eq!(builder.manifest.get_license_file(), Some("legal/LICENSE"));
958 }
959
960 #[test]
961 fn BundleBuilder___add_license_file___nonexistent___returns_error() {
962 let manifest = Manifest::new("test", "1.0.0");
963 let result = BundleBuilder::new(manifest).add_license_file("/nonexistent/LICENSE");
964
965 assert!(result.is_err());
966 }
967}