44
55use crate :: logging:: LogTimer ;
66use crate :: plugin_paths:: { built_in_plugin_dirs, ensure_dir, plugins_dir, PLUGIN_MANIFEST_NAME } ;
7+ use flate2:: read:: GzDecoder ;
78use log:: { debug, error, info, trace, warn} ;
89use serde:: { Deserialize , Serialize } ;
910use sha2:: { Digest , Sha256 } ;
1011use std:: collections:: { BTreeMap , HashSet } ;
1112use std:: fs;
12- use std:: io:: { Read , Write } ;
13+ use std:: io:: { Read , Seek , SeekFrom , Write } ;
1314use std:: path:: { Component , Path , PathBuf } ;
1415use std:: sync:: OnceLock ;
15- use xz2:: read:: XzDecoder ;
1616
1717const MODULE : & str = "plugin_bundles" ;
18+ const GZIP_MAGIC : [ u8 ; 2 ] = [ 0x1F , 0x8B ] ;
19+
20+ /// Opens a plugin bundle as a decompressed tar reader.
21+ ///
22+ /// `.ovcsp` bundles are gzip-compressed tar archives.
23+ ///
24+ /// # Parameters
25+ /// - `bundle_path`: Bundle file path.
26+ ///
27+ /// # Returns
28+ /// - `Ok(Box<dyn Read>)` with a decompressed tar stream.
29+ /// - `Err(String)` when the bundle cannot be opened or has an unknown format.
30+ fn open_bundle_reader ( bundle_path : & Path ) -> Result < Box < dyn Read > , String > {
31+ let mut file =
32+ fs:: File :: open ( bundle_path) . map_err ( |e| format ! ( "open {}: {e}" , bundle_path. display( ) ) ) ?;
33+ let mut magic = [ 0u8 ; 6 ] ;
34+ let read = file
35+ . read ( & mut magic)
36+ . map_err ( |e| format ! ( "read {}: {e}" , bundle_path. display( ) ) ) ?;
37+ file. seek ( SeekFrom :: Start ( 0 ) )
38+ . map_err ( |e| format ! ( "seek {}: {e}" , bundle_path. display( ) ) ) ?;
39+
40+ if read >= GZIP_MAGIC . len ( ) && magic[ ..GZIP_MAGIC . len ( ) ] == GZIP_MAGIC {
41+ return Ok ( Box :: new ( GzDecoder :: new ( file) ) ) ;
42+ }
43+ Err ( format ! (
44+ "unsupported bundle compression in {}" ,
45+ bundle_path. display( )
46+ ) )
47+ }
48+
49+ /// Opens a plugin bundle as a tar archive.
50+ ///
51+ /// # Parameters
52+ /// - `bundle_path`: Bundle file path.
53+ ///
54+ /// # Returns
55+ /// - `Ok(tar::Archive<...>)` when the bundle format is supported.
56+ /// - `Err(String)` when the bundle cannot be decoded.
57+ fn open_bundle_archive ( bundle_path : & Path ) -> Result < tar:: Archive < Box < dyn Read > > , String > {
58+ let reader = open_bundle_reader ( bundle_path) ?;
59+ Ok ( tar:: Archive :: new ( reader) )
60+ }
1861
1962/// Safety and resource limits enforced during bundle extraction.
2063#[ derive( Debug , Clone , Copy ) ]
@@ -326,7 +369,7 @@ impl PluginBundleStore {
326369 & bundle_sha256[ ..12 ]
327370 ) ;
328371
329- let ( manifest_bundle_path, manifest) = locate_manifest_tar_xz ( bundle_path) ?;
372+ let ( manifest_bundle_path, manifest) = locate_manifest_in_bundle ( bundle_path) ?;
330373 let plugin_id = manifest. id . trim ( ) . to_string ( ) ;
331374 if plugin_id. is_empty ( ) {
332375 error ! ( "install_ovcsp_with_limits: manifest id is empty" , ) ;
@@ -385,10 +428,8 @@ impl PluginBundleStore {
385428 . map_err ( |e| format ! ( "canonicalize {}: {e}" , staging_version_dir. display( ) ) ) ?;
386429
387430 // Extract all entries under `<pluginId>/...` into the staging version directory.
388- let f = fs:: File :: open ( bundle_path)
389- . map_err ( |e| format ! ( "open {}: {e}" , bundle_path. display( ) ) ) ?;
390- let decoder = XzDecoder :: new ( f) ;
391- let mut tar = tar:: Archive :: new ( decoder) ;
431+ let mut tar = open_bundle_archive ( bundle_path)
432+ . map_err ( |e| format ! ( "open bundle archive {}: {e}" , bundle_path. display( ) ) ) ?;
392433
393434 for entry in tar. entries ( ) . map_err ( |e| format ! ( "read tar: {e}" ) ) ? {
394435 let mut entry = entry. map_err ( |e| format ! ( "tar entry: {e}" ) ) ?;
@@ -696,7 +737,7 @@ impl PluginBundleStore {
696737 trace ! ( "ensure_built_in_bundle: {}" , bundle_path. display( ) ) ;
697738
698739 let bundle_sha256 = sha256_hex_file ( bundle_path) ?;
699- let ( _manifest_path, manifest) = locate_manifest_tar_xz ( bundle_path) ?;
740+ let ( _manifest_path, manifest) = locate_manifest_in_bundle ( bundle_path) ?;
700741 let plugin_id = manifest. id . trim ( ) ;
701742 if plugin_id. is_empty ( ) {
702743 error ! ( "ensure_built_in_bundle: bundle manifest id is empty" , ) ;
@@ -1171,7 +1212,7 @@ fn read_built_in_plugin_ids() -> HashSet<String> {
11711212 let mut out: HashSet < String > = HashSet :: new ( ) ;
11721213
11731214 for bundle_path in builtin_bundle_paths ( ) {
1174- let ( _manifest_path, manifest) = match locate_manifest_tar_xz ( & bundle_path) {
1215+ let ( _manifest_path, manifest) = match locate_manifest_in_bundle ( & bundle_path) {
11751216 Ok ( v) => v,
11761217 Err ( err) => {
11771218 warn ! (
@@ -1240,19 +1281,17 @@ fn derive_install_version(manifest: &PluginManifest, bundle_sha256: &str) -> Str
12401281 . unwrap_or_else ( || format ! ( "sha256-{}" , & bundle_sha256[ ..12 ] ) )
12411282}
12421283
1243- /// Finds and parses `openvcs.plugin.json` from a tar.xz bundle.
1284+ /// Finds and parses `openvcs.plugin.json` from a plugin bundle archive .
12441285///
12451286/// # Parameters
12461287/// - `bundle_path`: Bundle file path.
12471288///
12481289/// # Returns
12491290/// - `Ok((PathBuf, PluginManifest))` manifest path inside archive and manifest payload.
12501291/// - `Err(String)` on read/parse/validation failure.
1251- fn locate_manifest_tar_xz ( bundle_path : & Path ) -> Result < ( PathBuf , PluginManifest ) , String > {
1252- let file =
1253- fs:: File :: open ( bundle_path) . map_err ( |e| format ! ( "open {}: {e}" , bundle_path. display( ) ) ) ?;
1254- let decoder = XzDecoder :: new ( file) ;
1255- let mut tar = tar:: Archive :: new ( decoder) ;
1292+ fn locate_manifest_in_bundle ( bundle_path : & Path ) -> Result < ( PathBuf , PluginManifest ) , String > {
1293+ let mut tar = open_bundle_archive ( bundle_path)
1294+ . map_err ( |e| format ! ( "open bundle archive {}: {e}" , bundle_path. display( ) ) ) ?;
12561295
12571296 let mut manifest_path: Option < PathBuf > = None ;
12581297 let mut manifest_json: Option < Vec < u8 > > = None ;
@@ -1382,9 +1421,9 @@ fn validate_entrypoint(version_dir: &Path, exec: Option<&str>, label: &str) -> R
13821421#[ cfg( test) ]
13831422mod tests {
13841423 use super :: * ;
1385- use std:: io:: Cursor ;
1424+ use flate2:: write:: GzEncoder ;
1425+ use flate2:: Compression ;
13861426 use tempfile:: tempdir;
1387- use xz2:: write:: XzEncoder ;
13881427
13891428 /// Synthetic tar entry kind used by bundle-construction helpers.
13901429 enum TarEntryKind {
@@ -1406,16 +1445,15 @@ mod tests {
14061445 kind : TarEntryKind ,
14071446 }
14081447
1409- /// Builds a tar.xz bundle from synthetic test entries.
1448+ /// Builds a tar.gz bundle from synthetic test entries.
14101449 ///
14111450 /// # Parameters
14121451 /// - `entries`: Tar entries to include.
14131452 ///
14141453 /// # Returns
1415- /// - Encoded tar.xz bytes.
1416- fn make_tar_xz_bundle ( entries : Vec < TarEntry > ) -> Vec < u8 > {
1417- let cursor = Cursor :: new ( Vec :: < u8 > :: new ( ) ) ;
1418- let encoder = XzEncoder :: new ( cursor, 6 ) ;
1454+ /// - Encoded tar.gz bytes.
1455+ fn make_tar_gz_bundle ( entries : Vec < TarEntry > ) -> Vec < u8 > {
1456+ let encoder = GzEncoder :: new ( Vec :: < u8 > :: new ( ) , Compression :: default ( ) ) ;
14191457 let mut tar = tar:: Builder :: new ( encoder) ;
14201458
14211459 for e in entries {
@@ -1446,17 +1484,17 @@ mod tests {
14461484 }
14471485
14481486 let encoder = tar. into_inner ( ) . unwrap ( ) ;
1449- encoder. finish ( ) . unwrap ( ) . into_inner ( )
1487+ encoder. finish ( ) . unwrap ( )
14501488 }
14511489
1452- /// Builds a minimal raw tar.xz bundle from `(path, bytes)` tuples.
1490+ /// Builds a minimal raw tar.gz bundle from `(path, bytes)` tuples.
14531491 ///
14541492 /// # Parameters
14551493 /// - `entries`: Raw path/data entries.
14561494 ///
14571495 /// # Returns
1458- /// - Encoded tar.xz bytes.
1459- fn make_raw_tar_xz_bundle ( entries : Vec < ( String , Vec < u8 > ) > ) -> Vec < u8 > {
1496+ /// - Encoded tar.gz bytes.
1497+ fn make_raw_tar_gz_bundle ( entries : Vec < ( String , Vec < u8 > ) > ) -> Vec < u8 > {
14601498 /// Writes an octal tar header field.
14611499 ///
14621500 /// # Parameters
@@ -1524,7 +1562,7 @@ mod tests {
15241562 tar_bytes. resize ( tar_bytes. len ( ) + 1024 , 0u8 ) ;
15251563
15261564 let mut out = Vec :: < u8 > :: new ( ) ;
1527- let mut enc = XzEncoder :: new ( & mut out, 6 ) ;
1565+ let mut enc = GzEncoder :: new ( & mut out, Compression :: default ( ) ) ;
15281566 enc. write_all ( & tar_bytes) . unwrap ( ) ;
15291567 enc. finish ( ) . unwrap ( ) ;
15301568 out
@@ -1576,7 +1614,7 @@ mod tests {
15761614 kind : TarEntryKind :: File ,
15771615 } ) ;
15781616 }
1579- let bundle = make_tar_xz_bundle ( entries) ;
1617+ let bundle = make_tar_gz_bundle ( entries) ;
15801618
15811619 let ( _tmp, bundle_path) = write_bundle_to_temp ( & bundle) ;
15821620 let store_root = tempdir ( ) . unwrap ( ) ;
@@ -1598,7 +1636,7 @@ mod tests {
15981636 /// # Returns
15991637 /// - `()`.
16001638 fn install_requires_manifest_at_expected_location ( ) {
1601- let bundle = make_tar_xz_bundle ( vec ! [ TarEntry {
1639+ let bundle = make_tar_gz_bundle ( vec ! [ TarEntry {
16021640 name: "test.plugin/other.json" . into( ) ,
16031641 data: b"{}" . to_vec( ) ,
16041642 unix_mode: None ,
@@ -1619,7 +1657,7 @@ mod tests {
16191657 /// # Returns
16201658 /// - `()`.
16211659 fn install_validates_declared_entrypoints_exist ( ) {
1622- let bundle = make_tar_xz_bundle ( vec ! [ TarEntry {
1660+ let bundle = make_tar_gz_bundle ( vec ! [ TarEntry {
16231661 name: "test.plugin/openvcs.plugin.json" . into( ) ,
16241662 data: basic_manifest(
16251663 "test.plugin" ,
@@ -1643,7 +1681,7 @@ mod tests {
16431681 /// # Returns
16441682 /// - `()`.
16451683 fn install_rejects_functions_component ( ) {
1646- let bundle = make_tar_xz_bundle ( vec ! [ TarEntry {
1684+ let bundle = make_tar_gz_bundle ( vec ! [ TarEntry {
16471685 name: "test.plugin/openvcs.plugin.json" . into( ) ,
16481686 data: basic_manifest( "test.plugin" , ",\" functions\" :{\" exec\" :\" legacy.mjs\" }" ) ,
16491687 unix_mode: None ,
@@ -1662,12 +1700,12 @@ mod tests {
16621700 }
16631701
16641702 #[ test]
1665- /// Verifies installer accepts valid tar.xz bundles.
1703+ /// Verifies installer accepts valid tar.gz bundles.
16661704 ///
16671705 /// # Returns
16681706 /// - `()`.
1669- fn install_accepts_tar_xz_bundles ( ) {
1670- let bundle = make_tar_xz_bundle ( vec ! [
1707+ fn install_accepts_tar_gz_bundles ( ) {
1708+ let bundle = make_tar_gz_bundle ( vec ! [
16711709 TarEntry {
16721710 name: "test.plugin/openvcs.plugin.json" . into( ) ,
16731711 data: basic_manifest(
@@ -1700,7 +1738,7 @@ mod tests {
17001738 /// # Returns
17011739 /// - `()`.
17021740 fn install_rejects_tar_zipslip_parent_dir ( ) {
1703- let bundle = make_raw_tar_xz_bundle ( vec ! [
1741+ let bundle = make_raw_tar_gz_bundle ( vec ! [
17041742 (
17051743 "test.plugin/openvcs.plugin.json" . into( ) ,
17061744 basic_manifest( "test.plugin" , "" ) ,
@@ -1722,7 +1760,7 @@ mod tests {
17221760 /// # Returns
17231761 /// - `()`.
17241762 fn install_rejects_tar_symlink_entries ( ) {
1725- let bundle = make_tar_xz_bundle ( vec ! [
1763+ let bundle = make_tar_gz_bundle ( vec ! [
17261764 TarEntry {
17271765 name: "test.plugin/openvcs.plugin.json" . into( ) ,
17281766 data: basic_manifest( "test.plugin" , "" ) ,
@@ -1754,7 +1792,7 @@ mod tests {
17541792 /// - `()`.
17551793 fn install_rejects_tar_suspicious_compression_ratio ( ) {
17561794 let big = vec ! [ 0u8 ; 2 * 1024 * 1024 ] ;
1757- let bundle = make_tar_xz_bundle ( vec ! [
1795+ let bundle = make_tar_gz_bundle ( vec ! [
17581796 TarEntry {
17591797 name: "test.plugin/openvcs.plugin.json" . into( ) ,
17601798 data: basic_manifest( "test.plugin" , "" ) ,
0 commit comments