1use 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
22pub 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
49fn 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 {
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 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 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}