Skip to main content

pg_embed/
pg_unpack.rs

1//! Unpacks the PostgreSQL binaries JAR into the binary cache.
2//!
3//! The JAR (a ZIP archive) distributed by
4//! [zonkyio/embedded-postgres-binaries](https://github.com/zonkyio/embedded-postgres-binaries)
5//! contains a single `.txz`-compressed tarball (a tar archive compressed with
6//! XZ/LZMA2, sometimes also named `.tar.xz`).  [`unpack_postgres`] locates
7//! that entry, decompresses it with [`lzma_rs`], and extracts the resulting
8//! tar archive into `cache_dir`.
9//!
10//! All I/O runs inside [`tokio::task::spawn_blocking`] so it does not block
11//! the async executor.
12
13use std::fs;
14use std::io::{Cursor, Read};
15use std::path::Path;
16
17use tar::Archive;
18use zip::ZipArchive;
19
20use crate::pg_errors::{Error, Result};
21
22/// Unpacks the PostgreSQL binaries ZIP/JAR into `cache_dir`.
23///
24/// Spawns a blocking task that opens `zip_file_path`, finds the `.txz` entry
25/// (XZ-compressed tarball), decompresses it, and extracts the tar archive into
26/// `cache_dir`.
27///
28/// # Arguments
29///
30/// * `zip_file_path` — Path to the downloaded JAR file.
31/// * `cache_dir` — Destination directory for the extracted binaries.
32///
33/// # Errors
34///
35/// Returns [`Error::ReadFileError`] if the ZIP file cannot be opened.
36/// Returns [`Error::InvalidPgPackage`] if the archive is malformed or an
37/// entry cannot be read.
38/// Returns [`Error::UnpackFailure`] if XZ decompression or tar extraction
39/// fails.
40/// Returns [`Error::PgError`] if the blocking task panics or cannot be joined.
41pub async fn unpack_postgres(zip_file_path: &Path, cache_dir: &Path) -> Result<()> {
42    let zip_file_path = zip_file_path.to_path_buf();
43    let cache_dir = cache_dir.to_path_buf();
44    tokio::task::spawn_blocking(move || unpack_postgres_blocking(&zip_file_path, &cache_dir))
45        .await
46        .map_err(|e| Error::PgError(e.to_string(), "spawn_blocking join error".into()))?
47}
48
49/// Blocking implementation of the unpack logic.
50fn unpack_postgres_blocking(zip_file_path: &Path, cache_dir: &Path) -> Result<()> {
51    let zip_file =
52        fs::File::open(zip_file_path).map_err(|e| Error::ReadFileError(e.to_string()))?;
53    let mut jar_archive =
54        ZipArchive::new(zip_file).map_err(|_| Error::InvalidPgPackage)?;
55
56    for i in 0..jar_archive.len() {
57        let mut file = jar_archive
58            .by_index(i)
59            .map_err(|_| Error::InvalidPgPackage)?;
60
61        if file.name().ends_with(".txz") || file.name().ends_with(".xz") {
62            let mut xz_content = Vec::with_capacity(file.compressed_size() as usize);
63            file.read_to_end(&mut xz_content)
64                .map_err(|e| Error::ReadFileError(e.to_string()))?;
65
66            let mut tar_content = Vec::new();
67            lzma_rs::xz_decompress(&mut Cursor::new(&xz_content), &mut tar_content)
68                .map_err(|_| Error::UnpackFailure)?;
69
70            Archive::new(Cursor::new(tar_content))
71                .unpack(cache_dir)
72                .map_err(|_| Error::UnpackFailure)?;
73        }
74    }
75
76    Ok(())
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use std::fs::File;
83    use std::io::Write;
84    use tempfile::tempdir;
85    use zip::write::{SimpleFileOptions, ZipWriter};
86
87    #[tokio::test]
88    async fn test_unpack_postgres() -> Result<()> {
89        let temp_dir = tempdir().expect("Failed to create temp dir");
90        let cache_dir = temp_dir.path().join("cache");
91        let zip_file_path = temp_dir.path().join("test_archive.zip");
92
93        // Build a zip containing an xz-compressed tarball
94        {
95            let tar_content = create_dummy_tar_content();
96            let xz_content = compress_with_xz(&tar_content);
97
98            let zip_file = File::create(&zip_file_path).expect("Failed to create zip file");
99            let mut zip_writer = ZipWriter::new(zip_file);
100
101            zip_writer
102                .start_file("postgres-test.txz", SimpleFileOptions::default())
103                .expect("Failed to start zip entry");
104
105            zip_writer
106                .write_all(&xz_content)
107                .expect("Failed to write compressed content to zip file");
108
109            zip_writer.finish().expect("Failed to finish zip file");
110        }
111
112        let result = unpack_postgres(&zip_file_path, &cache_dir).await;
113        assert!(result.is_ok(), "unpack_postgres should succeed: {:?}", result);
114
115        let unpacked_files: Vec<_> = std::fs::read_dir(&cache_dir)
116            .expect("Failed to read unpacked directory")
117            .collect();
118
119        assert!(
120            !unpacked_files.is_empty(),
121            "cache_dir should contain the unpacked files"
122        );
123
124        Ok(())
125    }
126
127    /// Create a minimal tar archive containing a single dummy file
128    fn create_dummy_tar_content() -> Vec<u8> {
129        let mut tar_data = Vec::new();
130        {
131            let mut ar = tar::Builder::new(&mut tar_data);
132            let content = b"Hello, Postgres!";
133            let mut header = tar::Header::new_gnu();
134            header.set_size(content.len() as u64);
135            header.set_cksum();
136            ar.append_data(&mut header, "dummy_file.txt", &content[..])
137                .expect("Failed to add file to tar");
138        }
139        tar_data
140    }
141
142    /// Compress `data` using XZ (LZMA2) via lzma-rs
143    fn compress_with_xz(data: &[u8]) -> Vec<u8> {
144        let mut compressed = Vec::new();
145        lzma_rs::xz_compress(&mut Cursor::new(data), &mut compressed)
146            .expect("Failed to compress data with xz");
147        compressed
148    }
149}