exarch_core/
test_utils.rs

1//! Test utilities for archive creation and validation.
2//!
3//! This module provides reusable helpers for creating in-memory test archives,
4//! reducing code duplication across format-specific tests.
5//!
6//! # Panics
7//!
8//! All functions in this module may panic on I/O errors since they are
9//! designed for test use only where panics are acceptable.
10
11#![allow(clippy::unwrap_used, clippy::missing_panics_doc)]
12
13use std::io::Cursor;
14use std::io::Write;
15
16/// Creates an in-memory TAR archive from a list of entries.
17///
18/// Each entry is a tuple of (path, content). Files are created with mode 0o644.
19///
20/// # Examples
21///
22/// ```
23/// use exarch_core::test_utils::create_test_tar;
24///
25/// let tar_data = create_test_tar(vec![("file.txt", b"hello"), ("dir/nested.txt", b"world")]);
26/// ```
27#[must_use]
28pub fn create_test_tar(entries: Vec<(&str, &[u8])>) -> Vec<u8> {
29    let mut ar = tar::Builder::new(Vec::new());
30    for (path, data) in entries {
31        let mut header = tar::Header::new_gnu();
32        header.set_size(data.len() as u64);
33        header.set_mode(0o644);
34        header.set_cksum();
35        ar.append_data(&mut header, path, data).unwrap();
36    }
37    ar.into_inner().unwrap()
38}
39
40/// Creates an in-memory ZIP archive from a list of entries.
41///
42/// Each entry is a tuple of (path, content). Files are stored uncompressed
43/// with mode 0o644.
44///
45/// # Examples
46///
47/// ```
48/// use exarch_core::test_utils::create_test_zip;
49///
50/// let zip_data = create_test_zip(vec![("file.txt", b"hello"), ("dir/nested.txt", b"world")]);
51/// ```
52#[must_use]
53pub fn create_test_zip(entries: Vec<(&str, &[u8])>) -> Vec<u8> {
54    use zip::write::SimpleFileOptions;
55    use zip::write::ZipWriter;
56
57    let buffer = Vec::new();
58    let mut zip = ZipWriter::new(Cursor::new(buffer));
59
60    let options = SimpleFileOptions::default()
61        .compression_method(zip::CompressionMethod::Stored)
62        .unix_permissions(0o644);
63
64    for (path, data) in entries {
65        zip.start_file(path, options).unwrap();
66        zip.write_all(data).unwrap();
67    }
68
69    zip.finish().unwrap().into_inner()
70}
71
72/// Builder for creating TAR test archives with various entry types.
73///
74/// Supports files, directories, symlinks, and hardlinks.
75///
76/// # Examples
77///
78/// ```
79/// use exarch_core::test_utils::TarTestBuilder;
80///
81/// let tar_data = TarTestBuilder::new()
82///     .add_file("file.txt", b"content")
83///     .add_directory("dir/")
84///     .add_symlink("link", "file.txt")
85///     .build();
86/// ```
87pub struct TarTestBuilder {
88    builder: tar::Builder<Vec<u8>>,
89}
90
91impl TarTestBuilder {
92    /// Creates a new TAR test builder.
93    #[must_use]
94    pub fn new() -> Self {
95        Self {
96            builder: tar::Builder::new(Vec::new()),
97        }
98    }
99
100    /// Adds a regular file to the archive.
101    #[must_use]
102    pub fn add_file(mut self, path: &str, data: &[u8]) -> Self {
103        let mut header = tar::Header::new_gnu();
104        header.set_size(data.len() as u64);
105        header.set_mode(0o644);
106        header.set_cksum();
107        self.builder.append_data(&mut header, path, data).unwrap();
108        self
109    }
110
111    /// Adds a regular file with custom mode.
112    #[must_use]
113    pub fn add_file_with_mode(mut self, path: &str, data: &[u8], mode: u32) -> Self {
114        let mut header = tar::Header::new_gnu();
115        header.set_size(data.len() as u64);
116        header.set_mode(mode);
117        header.set_cksum();
118        self.builder.append_data(&mut header, path, data).unwrap();
119        self
120    }
121
122    /// Adds a directory to the archive.
123    #[must_use]
124    pub fn add_directory(mut self, path: &str) -> Self {
125        let mut header = tar::Header::new_gnu();
126        header.set_size(0);
127        header.set_mode(0o755);
128        header.set_entry_type(tar::EntryType::Directory);
129        header.set_cksum();
130        self.builder
131            .append_data(&mut header, path, std::io::empty())
132            .unwrap();
133        self
134    }
135
136    /// Adds a symlink to the archive.
137    #[must_use]
138    pub fn add_symlink(mut self, path: &str, target: &str) -> Self {
139        let mut header = tar::Header::new_gnu();
140        header.set_size(0);
141        header.set_mode(0o777);
142        header.set_entry_type(tar::EntryType::Symlink);
143        header.set_link_name(target).unwrap();
144        header.set_cksum();
145        self.builder
146            .append_data(&mut header, path, std::io::empty())
147            .unwrap();
148        self
149    }
150
151    /// Adds a hardlink to the archive.
152    #[must_use]
153    pub fn add_hardlink(mut self, path: &str, target: &str) -> Self {
154        let mut header = tar::Header::new_gnu();
155        header.set_size(0);
156        header.set_mode(0o644);
157        header.set_entry_type(tar::EntryType::Link);
158        header.set_link_name(target).unwrap();
159        header.set_cksum();
160        self.builder
161            .append_data(&mut header, path, std::io::empty())
162            .unwrap();
163        self
164    }
165
166    /// Builds and returns the TAR archive data.
167    #[must_use]
168    pub fn build(self) -> Vec<u8> {
169        self.builder.into_inner().unwrap()
170    }
171}
172
173impl Default for TarTestBuilder {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179/// Builder for creating ZIP test archives with various entry types.
180///
181/// # Examples
182///
183/// ```
184/// use exarch_core::test_utils::ZipTestBuilder;
185///
186/// let zip_data = ZipTestBuilder::new()
187///     .add_file("file.txt", b"content")
188///     .add_directory("dir/")
189///     .build();
190/// ```
191pub struct ZipTestBuilder {
192    zip: zip::ZipWriter<Cursor<Vec<u8>>>,
193}
194
195impl ZipTestBuilder {
196    /// Creates a new ZIP test builder.
197    #[must_use]
198    pub fn new() -> Self {
199        Self {
200            zip: zip::ZipWriter::new(Cursor::new(Vec::new())),
201        }
202    }
203
204    /// Adds a regular file to the archive.
205    #[must_use]
206    pub fn add_file(mut self, path: &str, data: &[u8]) -> Self {
207        use zip::write::SimpleFileOptions;
208
209        let options = SimpleFileOptions::default()
210            .compression_method(zip::CompressionMethod::Stored)
211            .unix_permissions(0o644);
212
213        self.zip.start_file(path, options).unwrap();
214        self.zip.write_all(data).unwrap();
215        self
216    }
217
218    /// Adds a regular file with custom mode.
219    #[must_use]
220    pub fn add_file_with_mode(mut self, path: &str, data: &[u8], mode: u32) -> Self {
221        use zip::write::SimpleFileOptions;
222
223        let options = SimpleFileOptions::default()
224            .compression_method(zip::CompressionMethod::Stored)
225            .unix_permissions(mode);
226
227        self.zip.start_file(path, options).unwrap();
228        self.zip.write_all(data).unwrap();
229        self
230    }
231
232    /// Adds a directory to the archive.
233    #[must_use]
234    pub fn add_directory(mut self, path: &str) -> Self {
235        use zip::write::SimpleFileOptions;
236
237        let options = SimpleFileOptions::default().unix_permissions(0o755);
238        self.zip.add_directory(path, options).unwrap();
239        self
240    }
241
242    /// Adds a symlink to the archive.
243    #[cfg(unix)]
244    #[must_use]
245    pub fn add_symlink(mut self, path: &str, target: &str) -> Self {
246        use zip::write::SimpleFileOptions;
247
248        // ZIP stores symlinks as files with Unix mode bit set
249        let options = SimpleFileOptions::default().unix_permissions(0o120_777);
250
251        self.zip.start_file(path, options).unwrap();
252        self.zip.write_all(target.as_bytes()).unwrap();
253        self
254    }
255
256    /// Builds and returns the ZIP archive data.
257    #[must_use]
258    pub fn build(self) -> Vec<u8> {
259        self.zip.finish().unwrap().into_inner()
260    }
261}
262
263impl Default for ZipTestBuilder {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_create_test_tar() {
275        let tar_data = create_test_tar(vec![("file.txt", b"hello")]);
276        assert!(!tar_data.is_empty());
277    }
278
279    #[test]
280    fn test_create_test_zip() {
281        let zip_data = create_test_zip(vec![("file.txt", b"hello")]);
282        assert!(!zip_data.is_empty());
283    }
284
285    #[test]
286    fn test_tar_builder() {
287        let tar_data = TarTestBuilder::new()
288            .add_file("file.txt", b"content")
289            .add_directory("dir/")
290            .build();
291        assert!(!tar_data.is_empty());
292    }
293
294    #[test]
295    fn test_zip_builder() {
296        let zip_data = ZipTestBuilder::new()
297            .add_file("file.txt", b"content")
298            .add_directory("dir/")
299            .build();
300        assert!(!zip_data.is_empty());
301    }
302}