Skip to main content

rustbridge_bundle/
loader.rs

1//! Bundle loading utilities.
2//!
3//! The [`BundleLoader`] provides functionality to load and extract plugin bundles.
4
5use crate::builder::verify_sha256;
6use crate::{BundleError, BundleResult, MANIFEST_FILE, Manifest, Platform};
7use std::fs::{self, File};
8use std::io::Read;
9use std::path::{Path, PathBuf};
10use zip::ZipArchive;
11
12/// Loader for plugin bundles.
13///
14/// # Example
15///
16/// ```no_run
17/// use rustbridge_bundle::loader::BundleLoader;
18///
19/// let mut loader = BundleLoader::open("my-plugin-1.0.0.rbp")?;
20/// let manifest = loader.manifest();
21///
22/// // Extract library for current platform to temp directory
23/// let library_path = loader.extract_library_for_current_platform("/tmp/plugins")?;
24/// # Ok::<(), rustbridge_bundle::BundleError>(())
25/// ```
26#[derive(Debug)]
27pub struct BundleLoader {
28    archive: ZipArchive<File>,
29    manifest: Manifest,
30}
31
32impl BundleLoader {
33    /// Open a bundle file for reading.
34    pub fn open<P: AsRef<Path>>(path: P) -> BundleResult<Self> {
35        let path = path.as_ref();
36        let file = File::open(path)?;
37        let mut archive = ZipArchive::new(file)?;
38
39        // Read and parse manifest
40        let manifest = {
41            let mut manifest_file = archive.by_name(MANIFEST_FILE).map_err(|_| {
42                BundleError::MissingFile(format!("{MANIFEST_FILE} not found in bundle"))
43            })?;
44
45            let mut manifest_json = String::new();
46            manifest_file.read_to_string(&mut manifest_json)?;
47            Manifest::from_json(&manifest_json)?
48        };
49
50        // Validate manifest
51        manifest.validate()?;
52
53        Ok(Self { archive, manifest })
54    }
55
56    /// Get the bundle manifest.
57    #[must_use]
58    pub fn manifest(&self) -> &Manifest {
59        &self.manifest
60    }
61
62    /// Check if the bundle supports the current platform.
63    #[must_use]
64    pub fn supports_current_platform(&self) -> bool {
65        Platform::current()
66            .map(|p| self.manifest.supports_platform(p))
67            .unwrap_or(false)
68    }
69
70    /// Get the platform info for the current platform.
71    #[must_use]
72    pub fn current_platform_info(&self) -> Option<&crate::PlatformInfo> {
73        Platform::current().and_then(|p| self.manifest.get_platform(p))
74    }
75
76    /// Extract the library for a specific platform to a directory.
77    ///
78    /// Extracts the release variant (default). For other variants,
79    /// use `extract_library_variant` instead.
80    ///
81    /// Returns the path to the extracted library file.
82    pub fn extract_library<P: AsRef<Path>>(
83        &mut self,
84        platform: Platform,
85        output_dir: P,
86    ) -> BundleResult<PathBuf> {
87        self.extract_library_variant(platform, "release", output_dir)
88    }
89
90    /// Extract a specific variant of the library for a platform.
91    ///
92    /// Returns the path to the extracted library file.
93    pub fn extract_library_variant<P: AsRef<Path>>(
94        &mut self,
95        platform: Platform,
96        variant: &str,
97        output_dir: P,
98    ) -> BundleResult<PathBuf> {
99        let output_dir = output_dir.as_ref();
100
101        // Get platform info from manifest
102        let platform_info = self.manifest.get_platform(platform).ok_or_else(|| {
103            BundleError::UnsupportedPlatform(format!(
104                "Platform {} not found in bundle",
105                platform.as_str()
106            ))
107        })?;
108
109        // Get the specific variant
110        let variant_info =
111            platform_info
112                .variant(variant)
113                .ok_or_else(|| BundleError::VariantNotFound {
114                    platform: platform.as_str().to_string(),
115                    variant: variant.to_string(),
116                })?;
117
118        let library_path = variant_info.library.clone();
119        let expected_checksum = variant_info.checksum.clone();
120
121        // Read the library from the archive
122        let contents = {
123            let mut library_file = self.archive.by_name(&library_path).map_err(|_| {
124                BundleError::MissingFile(format!("Library not found in bundle: {library_path}"))
125            })?;
126
127            let mut contents = Vec::new();
128            library_file.read_to_end(&mut contents)?;
129            contents
130        };
131
132        // Verify checksum
133        if !verify_sha256(&contents, &expected_checksum) {
134            let actual = crate::builder::compute_sha256(&contents);
135            return Err(BundleError::ChecksumMismatch {
136                path: library_path,
137                expected: expected_checksum,
138                actual: format!("sha256:{actual}"),
139            });
140        }
141
142        // Create output directory
143        fs::create_dir_all(output_dir)?;
144
145        // Determine output filename
146        let file_name = Path::new(&library_path)
147            .file_name()
148            .ok_or_else(|| BundleError::InvalidManifest("Invalid library path".to_string()))?;
149
150        let output_path = output_dir.join(file_name);
151
152        // Write the library file
153        fs::write(&output_path, &contents)?;
154
155        // Set executable permissions on Unix
156        #[cfg(unix)]
157        {
158            use std::os::unix::fs::PermissionsExt;
159            let mut perms = fs::metadata(&output_path)?.permissions();
160            perms.set_mode(0o755);
161            fs::set_permissions(&output_path, perms)?;
162        }
163
164        Ok(output_path)
165    }
166
167    /// List all available variants for a platform.
168    #[must_use]
169    pub fn list_variants(&self, platform: Platform) -> Vec<&str> {
170        self.manifest.list_variants(platform)
171    }
172
173    /// Check if a specific variant exists for a platform.
174    #[must_use]
175    pub fn has_variant(&self, platform: Platform, variant: &str) -> bool {
176        self.manifest
177            .get_platform(platform)
178            .map(|p| p.has_variant(variant))
179            .unwrap_or(false)
180    }
181
182    /// Get the build info from the manifest.
183    #[must_use]
184    pub fn build_info(&self) -> Option<&crate::BuildInfo> {
185        self.manifest.get_build_info()
186    }
187
188    /// Get the SBOM info from the manifest.
189    #[must_use]
190    pub fn sbom(&self) -> Option<&crate::Sbom> {
191        self.manifest.get_sbom()
192    }
193
194    /// Extract the library for the current platform to a directory.
195    ///
196    /// Returns the path to the extracted library file.
197    pub fn extract_library_for_current_platform<P: AsRef<Path>>(
198        &mut self,
199        output_dir: P,
200    ) -> BundleResult<PathBuf> {
201        let platform = Platform::current().ok_or_else(|| {
202            BundleError::UnsupportedPlatform("Current platform is not supported".to_string())
203        })?;
204
205        self.extract_library(platform, output_dir)
206    }
207
208    /// Read a file from the bundle as bytes.
209    pub fn read_file(&mut self, path: &str) -> BundleResult<Vec<u8>> {
210        let mut file = self
211            .archive
212            .by_name(path)
213            .map_err(|_| BundleError::MissingFile(format!("File not found in bundle: {path}")))?;
214
215        let mut contents = Vec::new();
216        file.read_to_end(&mut contents)?;
217        Ok(contents)
218    }
219
220    /// Read a file from the bundle as a string.
221    pub fn read_file_string(&mut self, path: &str) -> BundleResult<String> {
222        let mut file = self
223            .archive
224            .by_name(path)
225            .map_err(|_| BundleError::MissingFile(format!("File not found in bundle: {path}")))?;
226
227        let mut contents = String::new();
228        file.read_to_string(&mut contents)?;
229        Ok(contents)
230    }
231
232    /// List all files in the bundle.
233    #[must_use]
234    pub fn list_files(&self) -> Vec<String> {
235        (0..self.archive.len())
236            .filter_map(|i| self.archive.name_for_index(i).map(String::from))
237            .collect()
238    }
239
240    /// Check if a file exists in the bundle.
241    #[must_use]
242    pub fn has_file(&self, path: &str) -> bool {
243        self.archive.index_for_name(path).is_some()
244    }
245
246    /// Check if the bundle has signature files.
247    #[must_use]
248    pub fn has_signatures(&self) -> bool {
249        self.has_file("manifest.json.minisig")
250    }
251
252    /// Get the public key from the manifest.
253    #[must_use]
254    pub fn public_key(&self) -> Option<&str> {
255        self.manifest.public_key.as_deref()
256    }
257
258    /// Verify the manifest signature.
259    ///
260    /// Returns Ok(()) if verification succeeds, or an error if it fails.
261    /// If no public key is available, returns `BundleError::NoPublicKey`.
262    pub fn verify_manifest_signature(&mut self) -> BundleResult<()> {
263        self.verify_manifest_signature_with_key(None)
264    }
265
266    /// Verify the manifest signature with an optional key override.
267    ///
268    /// # Arguments
269    ///
270    /// * `public_key_override` - If provided, use this key instead of the manifest's key
271    pub fn verify_manifest_signature_with_key(
272        &mut self,
273        public_key_override: Option<&str>,
274    ) -> BundleResult<()> {
275        // Clone the public key to avoid borrow issues
276        let public_key = public_key_override
277            .map(String::from)
278            .or_else(|| self.manifest.public_key.clone())
279            .ok_or(BundleError::NoPublicKey)?;
280
281        // Read manifest data
282        let manifest_data = self.read_file(MANIFEST_FILE)?;
283
284        // Read signature
285        let sig_path = format!("{MANIFEST_FILE}.minisig");
286        let sig_data = self.read_file(&sig_path)?;
287        let signature = String::from_utf8(sig_data).map_err(|e| {
288            BundleError::SignatureVerificationFailed(format!("Invalid signature encoding: {e}"))
289        })?;
290
291        // Verify
292        verify_minisign_signature(&public_key, &manifest_data, &signature)
293    }
294
295    /// Verify a library signature.
296    ///
297    /// # Arguments
298    ///
299    /// * `library_path` - Path to the library within the bundle
300    /// * `library_data` - The library file contents
301    pub fn verify_library_signature(
302        &mut self,
303        library_path: &str,
304        library_data: &[u8],
305    ) -> BundleResult<()> {
306        self.verify_library_signature_with_key(library_path, library_data, None)
307    }
308
309    /// Verify a library signature with an optional key override.
310    pub fn verify_library_signature_with_key(
311        &mut self,
312        library_path: &str,
313        library_data: &[u8],
314        public_key_override: Option<&str>,
315    ) -> BundleResult<()> {
316        // Clone the public key to avoid borrow issues
317        let public_key = public_key_override
318            .map(String::from)
319            .or_else(|| self.manifest.public_key.clone())
320            .ok_or(BundleError::NoPublicKey)?;
321
322        // Read signature
323        let sig_path = format!("{library_path}.minisig");
324        let sig_data = self.read_file(&sig_path)?;
325        let signature = String::from_utf8(sig_data).map_err(|e| {
326            BundleError::SignatureVerificationFailed(format!("Invalid signature encoding: {e}"))
327        })?;
328
329        // Verify
330        verify_minisign_signature(&public_key, library_data, &signature)
331    }
332
333    /// Extract and verify a library for a platform.
334    ///
335    /// This method extracts the library and verifies both the checksum and signature.
336    ///
337    /// # Arguments
338    ///
339    /// * `platform` - Target platform
340    /// * `output_dir` - Directory to extract to
341    /// * `verify_signature` - Whether to verify the signature
342    /// * `public_key_override` - Optional public key override
343    pub fn extract_library_verified<P: AsRef<Path>>(
344        &mut self,
345        platform: Platform,
346        output_dir: P,
347        verify_signature: bool,
348        public_key_override: Option<&str>,
349    ) -> BundleResult<PathBuf> {
350        // Verify manifest signature first if requested
351        if verify_signature {
352            self.verify_manifest_signature_with_key(public_key_override)?;
353        }
354
355        let output_dir = output_dir.as_ref();
356
357        // Get platform info from manifest
358        let platform_info = self.manifest.get_platform(platform).ok_or_else(|| {
359            BundleError::UnsupportedPlatform(format!(
360                "Platform {} not found in bundle",
361                platform.as_str()
362            ))
363        })?;
364
365        // Get the release variant
366        let variant_info = platform_info
367            .release()
368            .ok_or_else(|| BundleError::VariantNotFound {
369                platform: platform.as_str().to_string(),
370                variant: "release".to_string(),
371            })?;
372
373        let library_path = variant_info.library.clone();
374        let expected_checksum = variant_info.checksum.clone();
375
376        // Read the library from the archive
377        let contents = self.read_file(&library_path)?;
378
379        // Verify checksum
380        if !verify_sha256(&contents, &expected_checksum) {
381            let actual = crate::builder::compute_sha256(&contents);
382            return Err(BundleError::ChecksumMismatch {
383                path: library_path,
384                expected: expected_checksum,
385                actual: format!("sha256:{actual}"),
386            });
387        }
388
389        // Verify library signature if requested
390        if verify_signature {
391            self.verify_library_signature_with_key(&library_path, &contents, public_key_override)?;
392        }
393
394        // Create output directory
395        fs::create_dir_all(output_dir)?;
396
397        // Determine output filename
398        let file_name = Path::new(&library_path)
399            .file_name()
400            .ok_or_else(|| BundleError::InvalidManifest("Invalid library path".to_string()))?;
401
402        let output_path = output_dir.join(file_name);
403
404        // Write the library file
405        fs::write(&output_path, &contents)?;
406
407        // Set executable permissions on Unix
408        #[cfg(unix)]
409        {
410            use std::os::unix::fs::PermissionsExt;
411            let mut perms = fs::metadata(&output_path)?.permissions();
412            perms.set_mode(0o755);
413            fs::set_permissions(&output_path, perms)?;
414        }
415
416        Ok(output_path)
417    }
418}
419
420/// Verify a minisign signature.
421///
422/// Minisign format:
423/// - Public key: 2 bytes algorithm ID ("Ed") + 8 bytes key ID + 32 bytes Ed25519 key
424/// - Signature: 2 bytes algorithm ID + 8 bytes key ID + 64 bytes signature
425///   - "ED" (0x45, 0x44) = prehashed with BLAKE2b-512
426///   - "Ed" (0x45, 0x64) = legacy non-prehashed
427fn verify_minisign_signature(
428    public_key_base64: &str,
429    data: &[u8],
430    signature_content: &str,
431) -> BundleResult<()> {
432    use minisign::{PublicKey, SignatureBox};
433
434    // Parse public key
435    let public_key = PublicKey::from_base64(public_key_base64).map_err(|e| {
436        BundleError::SignatureVerificationFailed(format!("Invalid public key: {e}"))
437    })?;
438
439    // Parse signature (second line of the minisig file)
440    let signature_box = SignatureBox::from_string(signature_content).map_err(|e| {
441        BundleError::SignatureVerificationFailed(format!("Invalid signature format: {e}"))
442    })?;
443
444    // Verify - minisign::verify handles prehashing automatically
445    let mut data_reader = std::io::Cursor::new(data);
446    minisign::verify(
447        &public_key,
448        &signature_box,
449        &mut data_reader,
450        true,
451        false,
452        false,
453    )
454    .map_err(|e| {
455        BundleError::SignatureVerificationFailed(format!("Signature verification failed: {e}"))
456    })?;
457
458    Ok(())
459}
460
461#[cfg(test)]
462mod tests {
463    #![allow(non_snake_case)]
464
465    use super::*;
466    use crate::builder::{BundleBuilder, compute_sha256};
467    use std::io::Write;
468    use tempfile::TempDir;
469
470    fn create_test_bundle(temp_dir: &TempDir) -> PathBuf {
471        let bundle_path = temp_dir.path().join("test.rbp");
472
473        // Create a fake library file
474        let lib_path = temp_dir.path().join("libtest.so");
475        fs::write(&lib_path, b"fake library contents").unwrap();
476
477        // Build the bundle
478        let manifest = Manifest::new("test-plugin", "1.0.0");
479        BundleBuilder::new(manifest)
480            .add_library(Platform::LinuxX86_64, &lib_path)
481            .unwrap()
482            .add_bytes("schema/messages.h", b"// header".to_vec())
483            .write(&bundle_path)
484            .unwrap();
485
486        bundle_path
487    }
488
489    fn create_multi_platform_bundle(temp_dir: &TempDir) -> PathBuf {
490        let bundle_path = temp_dir.path().join("multi.rbp");
491
492        let linux_lib = temp_dir.path().join("libtest.so");
493        let macos_lib = temp_dir.path().join("libtest.dylib");
494        let windows_lib = temp_dir.path().join("test.dll");
495        fs::write(&linux_lib, b"linux library").unwrap();
496        fs::write(&macos_lib, b"macos library").unwrap();
497        fs::write(&windows_lib, b"windows library").unwrap();
498
499        let manifest = Manifest::new("multi-platform", "2.0.0");
500        BundleBuilder::new(manifest)
501            .add_library(Platform::LinuxX86_64, &linux_lib)
502            .unwrap()
503            .add_library(Platform::DarwinAarch64, &macos_lib)
504            .unwrap()
505            .add_library(Platform::WindowsX86_64, &windows_lib)
506            .unwrap()
507            .write(&bundle_path)
508            .unwrap();
509
510        bundle_path
511    }
512
513    #[test]
514    fn BundleLoader___open___reads_manifest() {
515        let temp_dir = TempDir::new().unwrap();
516        let bundle_path = create_test_bundle(&temp_dir);
517
518        let loader = BundleLoader::open(&bundle_path).unwrap();
519
520        assert_eq!(loader.manifest().plugin.name, "test-plugin");
521        assert_eq!(loader.manifest().plugin.version, "1.0.0");
522    }
523
524    #[test]
525    fn BundleLoader___open___nonexistent_file___returns_error() {
526        let result = BundleLoader::open("/nonexistent/bundle.rbp");
527
528        assert!(result.is_err());
529    }
530
531    #[test]
532    fn BundleLoader___open___not_a_zip___returns_error() {
533        let temp_dir = TempDir::new().unwrap();
534        let fake_bundle = temp_dir.path().join("fake.rbp");
535        fs::write(&fake_bundle, b"not a zip file").unwrap();
536
537        let result = BundleLoader::open(&fake_bundle);
538
539        assert!(result.is_err());
540    }
541
542    #[test]
543    fn BundleLoader___open___missing_manifest___returns_error() {
544        let temp_dir = TempDir::new().unwrap();
545        let bundle_path = temp_dir.path().join("no-manifest.rbp");
546
547        // Create a ZIP without manifest.json
548        let file = File::create(&bundle_path).unwrap();
549        let mut zip = zip::ZipWriter::new(file);
550        let options = zip::write::SimpleFileOptions::default();
551        zip.start_file("some-file.txt", options).unwrap();
552        zip.write_all(b"content").unwrap();
553        zip.finish().unwrap();
554
555        let result = BundleLoader::open(&bundle_path);
556
557        assert!(result.is_err());
558        let err = result.unwrap_err();
559        assert!(matches!(err, BundleError::MissingFile(_)));
560        assert!(err.to_string().contains("manifest.json"));
561    }
562
563    #[test]
564    fn BundleLoader___open___invalid_manifest_json___returns_error() {
565        let temp_dir = TempDir::new().unwrap();
566        let bundle_path = temp_dir.path().join("bad-manifest.rbp");
567
568        // Create a ZIP with invalid JSON in manifest
569        let file = File::create(&bundle_path).unwrap();
570        let mut zip = zip::ZipWriter::new(file);
571        let options = zip::write::SimpleFileOptions::default();
572        zip.start_file("manifest.json", options).unwrap();
573        zip.write_all(b"{ invalid json }").unwrap();
574        zip.finish().unwrap();
575
576        let result = BundleLoader::open(&bundle_path);
577
578        assert!(result.is_err());
579    }
580
581    #[test]
582    fn BundleLoader___list_files___returns_all_files() {
583        let temp_dir = TempDir::new().unwrap();
584        let bundle_path = create_test_bundle(&temp_dir);
585
586        let loader = BundleLoader::open(&bundle_path).unwrap();
587        let files = loader.list_files();
588
589        assert!(files.contains(&"manifest.json".to_string()));
590        assert!(files.contains(&"schema/messages.h".to_string()));
591    }
592
593    #[test]
594    fn BundleLoader___has_file___returns_true_for_existing() {
595        let temp_dir = TempDir::new().unwrap();
596        let bundle_path = create_test_bundle(&temp_dir);
597
598        let loader = BundleLoader::open(&bundle_path).unwrap();
599
600        assert!(loader.has_file("manifest.json"));
601        assert!(loader.has_file("schema/messages.h"));
602        assert!(!loader.has_file("nonexistent.txt"));
603    }
604
605    #[test]
606    fn BundleLoader___read_file___returns_contents() {
607        let temp_dir = TempDir::new().unwrap();
608        let bundle_path = create_test_bundle(&temp_dir);
609
610        let mut loader = BundleLoader::open(&bundle_path).unwrap();
611        let contents = loader.read_file_string("schema/messages.h").unwrap();
612
613        assert_eq!(contents, "// header");
614    }
615
616    #[test]
617    fn BundleLoader___read_file___missing_file___returns_error() {
618        let temp_dir = TempDir::new().unwrap();
619        let bundle_path = create_test_bundle(&temp_dir);
620
621        let mut loader = BundleLoader::open(&bundle_path).unwrap();
622        let result = loader.read_file("nonexistent.txt");
623
624        assert!(result.is_err());
625        let err = result.unwrap_err();
626        assert!(matches!(err, BundleError::MissingFile(_)));
627    }
628
629    #[test]
630    fn BundleLoader___read_file___returns_bytes() {
631        let temp_dir = TempDir::new().unwrap();
632        let bundle_path = create_test_bundle(&temp_dir);
633
634        let mut loader = BundleLoader::open(&bundle_path).unwrap();
635        let contents = loader.read_file("schema/messages.h").unwrap();
636
637        assert_eq!(contents, b"// header");
638    }
639
640    #[test]
641    fn BundleLoader___extract_library___verifies_checksum() {
642        let temp_dir = TempDir::new().unwrap();
643        let bundle_path = create_test_bundle(&temp_dir);
644        let extract_dir = temp_dir.path().join("extracted");
645
646        let mut loader = BundleLoader::open(&bundle_path).unwrap();
647        let lib_path = loader
648            .extract_library(Platform::LinuxX86_64, &extract_dir)
649            .unwrap();
650
651        assert!(lib_path.exists());
652        let contents = fs::read(&lib_path).unwrap();
653        assert_eq!(contents, b"fake library contents");
654    }
655
656    #[test]
657    fn BundleLoader___extract_library___unsupported_platform___returns_error() {
658        let temp_dir = TempDir::new().unwrap();
659        let bundle_path = create_test_bundle(&temp_dir);
660        let extract_dir = temp_dir.path().join("extracted");
661
662        let mut loader = BundleLoader::open(&bundle_path).unwrap();
663        let result = loader.extract_library(Platform::WindowsX86_64, &extract_dir);
664
665        assert!(result.is_err());
666        let err = result.unwrap_err();
667        assert!(matches!(err, BundleError::UnsupportedPlatform(_)));
668    }
669
670    #[test]
671    fn BundleLoader___extract_library___creates_output_directory() {
672        let temp_dir = TempDir::new().unwrap();
673        let bundle_path = create_test_bundle(&temp_dir);
674        let extract_dir = temp_dir.path().join("deep").join("nested").join("dir");
675
676        let mut loader = BundleLoader::open(&bundle_path).unwrap();
677        let lib_path = loader
678            .extract_library(Platform::LinuxX86_64, &extract_dir)
679            .unwrap();
680
681        assert!(extract_dir.exists());
682        assert!(lib_path.exists());
683    }
684
685    #[test]
686    fn BundleLoader___multi_platform___extract_each_platform() {
687        let temp_dir = TempDir::new().unwrap();
688        let bundle_path = create_multi_platform_bundle(&temp_dir);
689
690        let mut loader = BundleLoader::open(&bundle_path).unwrap();
691
692        // Verify all three platforms are supported
693        assert!(loader.manifest().supports_platform(Platform::LinuxX86_64));
694        assert!(loader.manifest().supports_platform(Platform::DarwinAarch64));
695        assert!(loader.manifest().supports_platform(Platform::WindowsX86_64));
696
697        // Extract Linux
698        let linux_dir = temp_dir.path().join("linux");
699        let linux_lib = loader
700            .extract_library(Platform::LinuxX86_64, &linux_dir)
701            .unwrap();
702        assert_eq!(fs::read(&linux_lib).unwrap(), b"linux library");
703
704        // Extract macOS
705        let macos_dir = temp_dir.path().join("macos");
706        let macos_lib = loader
707            .extract_library(Platform::DarwinAarch64, &macos_dir)
708            .unwrap();
709        assert_eq!(fs::read(&macos_lib).unwrap(), b"macos library");
710
711        // Extract Windows
712        let windows_dir = temp_dir.path().join("windows");
713        let windows_lib = loader
714            .extract_library(Platform::WindowsX86_64, &windows_dir)
715            .unwrap();
716        assert_eq!(fs::read(&windows_lib).unwrap(), b"windows library");
717    }
718
719    #[test]
720    fn BundleLoader___supports_current_platform___returns_correct_value() {
721        let temp_dir = TempDir::new().unwrap();
722        let bundle_path = create_test_bundle(&temp_dir);
723
724        let loader = BundleLoader::open(&bundle_path).unwrap();
725
726        // This test will pass on Linux x86_64, fail on other platforms
727        // which is expected behavior
728        if Platform::current() == Some(Platform::LinuxX86_64) {
729            assert!(loader.supports_current_platform());
730        }
731    }
732
733    #[test]
734    fn BundleLoader___current_platform_info___returns_info_when_supported() {
735        let temp_dir = TempDir::new().unwrap();
736        let bundle_path = create_test_bundle(&temp_dir);
737
738        let loader = BundleLoader::open(&bundle_path).unwrap();
739
740        if Platform::current() == Some(Platform::LinuxX86_64) {
741            let info = loader.current_platform_info();
742            assert!(info.is_some());
743            let release = info.unwrap().release().unwrap();
744            assert!(release.library.contains("libtest.so"));
745        }
746    }
747
748    #[test]
749    fn roundtrip___create_and_load___preserves_all_data() {
750        let temp_dir = TempDir::new().unwrap();
751        let bundle_path = temp_dir.path().join("roundtrip.rbp");
752
753        // Create library
754        let lib_path = temp_dir.path().join("libplugin.so");
755        let lib_contents = b"roundtrip test library";
756        fs::write(&lib_path, lib_contents).unwrap();
757
758        // Create bundle with metadata
759        let mut manifest = Manifest::new("roundtrip-plugin", "3.2.1");
760        manifest.plugin.description = Some("A test plugin for roundtrip".to_string());
761        manifest.plugin.authors = vec!["Author One".to_string(), "Author Two".to_string()];
762        manifest.plugin.license = Some("MIT".to_string());
763
764        BundleBuilder::new(manifest)
765            .add_library(Platform::LinuxX86_64, &lib_path)
766            .unwrap()
767            .add_bytes("docs/README.md", b"# Documentation".to_vec())
768            .write(&bundle_path)
769            .unwrap();
770
771        // Load and verify
772        let mut loader = BundleLoader::open(&bundle_path).unwrap();
773
774        assert_eq!(loader.manifest().plugin.name, "roundtrip-plugin");
775        assert_eq!(loader.manifest().plugin.version, "3.2.1");
776        assert_eq!(
777            loader.manifest().plugin.description,
778            Some("A test plugin for roundtrip".to_string())
779        );
780        assert_eq!(loader.manifest().plugin.authors.len(), 2);
781        assert_eq!(loader.manifest().plugin.license, Some("MIT".to_string()));
782
783        // Verify checksum in manifest matches actual content
784        let platform_info = loader
785            .manifest()
786            .get_platform(Platform::LinuxX86_64)
787            .unwrap();
788        let release = platform_info.release().unwrap();
789        let expected_checksum = format!("sha256:{}", compute_sha256(lib_contents));
790        assert_eq!(release.checksum, expected_checksum);
791
792        // Verify library extraction
793        let extract_dir = temp_dir.path().join("extract");
794        let extracted = loader
795            .extract_library(Platform::LinuxX86_64, &extract_dir)
796            .unwrap();
797        assert_eq!(fs::read(&extracted).unwrap(), lib_contents);
798    }
799
800    #[test]
801    fn roundtrip___bundle_with_schemas___preserves_schema_info() {
802        let temp_dir = TempDir::new().unwrap();
803        let bundle_path = temp_dir.path().join("schema-bundle.rbp");
804
805        let lib_path = temp_dir.path().join("libtest.so");
806        let header_path = temp_dir.path().join("messages.h");
807        let json_path = temp_dir.path().join("schema.json");
808        fs::write(&lib_path, b"lib").unwrap();
809        fs::write(&header_path, b"#pragma once").unwrap();
810        fs::write(&json_path, b"{}").unwrap();
811
812        let manifest = Manifest::new("schema-plugin", "1.0.0");
813        BundleBuilder::new(manifest)
814            .add_library(Platform::LinuxX86_64, &lib_path)
815            .unwrap()
816            .add_schema_file(&header_path, "messages.h")
817            .unwrap()
818            .add_schema_file(&json_path, "schema.json")
819            .unwrap()
820            .write(&bundle_path)
821            .unwrap();
822
823        let mut loader = BundleLoader::open(&bundle_path).unwrap();
824
825        // Verify schemas are in manifest
826        assert_eq!(loader.manifest().schemas.len(), 2);
827        assert!(loader.manifest().schemas.contains_key("messages.h"));
828        assert!(loader.manifest().schemas.contains_key("schema.json"));
829
830        // Verify schema files can be read
831        assert!(loader.has_file("schema/messages.h"));
832        assert!(loader.has_file("schema/schema.json"));
833        assert_eq!(
834            loader.read_file_string("schema/messages.h").unwrap(),
835            "#pragma once"
836        );
837    }
838}