tar_light/
lib.rs

1//! Simple tar archive reader and writer library
2//!
3//! # Usage
4//!
5//! ## Packing files into a TAR archive
6//!
7//! ```rust
8//! use tar_light::pack;
9//!
10//! let files = vec!["testdata/file1.txt", "testdata/file2.txt"];
11//! 
12//! pack("archive.tar", &files);
13//! // Creates archive.tar containing file1.txt and file2.txt
14//! pack("archive.tar.gz", &files);
15//! // Creates archive.tar.gz that is gzip-compressed
16//! ```
17//!
18//! ## Unpacking files from a TAR archive
19//!
20//! ```rust
21//! use tar_light::unpack;
22//!
23//! unpack("testdata/simple.tar", "output_directory");
24//! // Extracts all files from simple.tar to output_directory/
25//! unpack("testdata/simple.tar.gz", "output_directory");
26//! // Extracts all files from simple.tar.gz that is gzip-compressed
27//! ```
28//!
29//! ## Listing files in a TAR archive header
30//!
31//! ```rust
32//! use tar_light::list;
33//!
34//! match list("archive.tar") {
35//!     Ok(headers) => {
36//!         println!("Files in archive:");
37//!         for header in headers {
38//!             println!("  {} ({} bytes)", header.name, header.size);
39//!         }
40//!     }
41//!     Err(e) => eprintln!("Error: {}", e),
42//! }
43//! ```
44//!
45//! ## Listing files in a TAR entry
46//!
47//! ```rust
48//! use tar_light::list_entry;
49//!
50//! match list_entry("archive.tar") {
51//!     Ok(entries) => {
52//!         println!("Files in archive:");
53//!         for entry in entries {
54//!             println!("  {} ({} bytes)", entry.header.name, entry.header.size);
55//!         }
56//!     }
57//!     Err(e) => eprintln!("Error: {}", e),
58//! }
59//! ```
60//!
61//! ## Advanced usage with low-level API
62//!
63//! ```rust
64//! use tar_light::{read_tar, write_tar, Tar, TarEntry, TarHeader};
65//! use std::fs;
66//!
67//! // Read tar archives
68//! let bin_bytes = fs::read("testdata/simple.tar").unwrap();
69//! let entries = read_tar(&bin_bytes);
70//!
71//! // List entries
72//! for entry in &entries {
73//!     println!("{}: {} bytes", entry.header.name, entry.header.size);
74//! }
75//! 
76//! // Write entries
77//! let tar_bytes = write_tar(&entries);
78//! fs::write("archive.tar", tar_bytes).unwrap();
79//! 
80//! // Create tar archive from scratch
81//! let mut tar = Tar::new();
82//! tar.add_str_entry("file1.txt", "Hello, World!");
83//! tar.add_str_entry("file2.txt", "This is a test.");
84//! let tar_bytes = tar.to_bytes();
85//! fs::write("archive.tar", tar_bytes).unwrap();
86//! ```
87
88pub mod tar;
89
90use std::fs;
91use std::os::unix::fs::MetadataExt;
92use std::path::Path;
93use std::io::{Write, Read};
94use flate2::read::GzDecoder;
95use flate2::write::GzEncoder;
96use flate2::Compression;
97use std::io::{self, BufRead};
98
99#[cfg(unix)]
100use std::ffi::CStr;
101
102pub use tar::{read_tar, write_tar, Tar, TarEntry, TarHeader};
103
104// ----------------------------------------------------------------
105// Helper functions for gzip compression/decompression
106// ----------------------------------------------------------------
107
108#[cfg(unix)]
109/// Get username from uid using libc
110fn get_username_from_uid(uid: u32) -> Option<String> {
111    unsafe {
112        let passwd = libc::getpwuid(uid);
113        if passwd.is_null() {
114            return None;
115        }
116        let name_ptr = (*passwd).pw_name;
117        if name_ptr.is_null() {
118            return None;
119        }
120        CStr::from_ptr(name_ptr)
121            .to_str()
122            .ok()
123            .map(|s| s.to_string())
124    }
125}
126
127#[cfg(unix)]
128/// Get group name from gid using libc
129fn get_groupname_from_gid(gid: u32) -> Option<String> {
130    unsafe {
131        let group = libc::getgrgid(gid);
132        if group.is_null() {
133            return None;
134        }
135        let name_ptr = (*group).gr_name;
136        if name_ptr.is_null() {
137            return None;
138        }
139        CStr::from_ptr(name_ptr)
140            .to_str()
141            .ok()
142            .map(|s| s.to_string())
143    }
144}
145
146#[cfg(not(unix))]
147/// Stub for non-Unix platforms
148fn get_username_from_uid(_uid: u32) -> Option<String> {
149    None
150}
151
152#[cfg(not(unix))]
153/// Stub for non-Unix platforms
154fn get_groupname_from_gid(_gid: u32) -> Option<String> {
155    None
156}
157
158// ----------------------------------------------------------------
159// Helper functions for gzip compression/decompression
160// ----------------------------------------------------------------
161/// Checks if filename indicates gzip compression
162fn is_gzipped(filename: &str) -> bool {
163    filename.ends_with(".tar.gz") || filename.ends_with(".tgz")
164}
165
166/// Decompresses gzipped data if the filename suggests it's compressed
167/// Returns the raw data unchanged if not gzipped
168fn ungzip(filename: &str, data: Vec<u8>) -> Result<Vec<u8>, std::io::Error> {
169    if is_gzipped(filename) {
170        let mut decoder = GzDecoder::new(&data[..]);
171        let mut decompressed = Vec::new();
172        decoder.read_to_end(&mut decompressed)?;
173        Ok(decompressed)
174    } else {
175        Ok(data)
176    }
177}
178
179/// Compresses data with gzip if the filename suggests it should be compressed
180/// Returns the raw data unchanged if not a gzip filename
181fn gzip(filename: &str, data: Vec<u8>) -> Result<Vec<u8>, std::io::Error> {
182    if is_gzipped(filename) {
183        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
184        encoder.write_all(&data)?;
185        encoder.finish()
186    } else {
187        Ok(data)
188    }
189}
190
191// ----------------------------------------------------------------
192// Helper functions for recursive directory packing
193// ----------------------------------------------------------------
194/// Adds a single file to entries
195fn add_file_to_entries(file_path: &Path, base_path: &Path, entries: &mut Vec<TarEntry>) {
196    let data = match fs::read(file_path) {
197        Ok(d) => d,
198        Err(e) => {
199            eprintln!("Error reading {}: {}", file_path.display(), e);
200            return;
201        }
202    };
203    
204    // Calculate relative path from base_path
205    let relative_path = file_path.strip_prefix(base_path)
206        .unwrap_or(file_path)
207        .to_string_lossy()
208        .to_string();
209
210    let mut header = TarHeader::new(
211        relative_path,
212        0o644,
213        data.len() as u64       
214    );
215    // get file metadata
216    match fs::metadata(file_path) {
217        Ok(m) => {
218            header.mode = m.mode() as u32;
219            header.mtime = m.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH)
220                .duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs();
221            header.gid = m.gid();
222            header.uid = m.uid();
223            // Set uname and gname from uid/gid
224            if let Some(uname) = get_username_from_uid(m.uid()) {
225                header.uname = uname;
226            }
227            if let Some(gname) = get_groupname_from_gid(m.gid()) {
228                header.gname = gname;
229            }
230        },
231        Err(e) => {
232            eprintln!("Error getting metadata for {}: {}", file_path.display(), e);
233            return;
234        }
235    };    let header_bytes = header.to_bytes();
236    
237    entries.push(TarEntry {
238        header,
239        data,
240        header_bytes,
241    });
242}
243
244/// Recursively collects all files from a directory
245fn collect_files_from_dir(dir_path: &Path, base_path: &Path, entries: &mut Vec<TarEntry>) {
246    let read_dir = match fs::read_dir(dir_path) {
247        Ok(d) => d,
248        Err(e) => {
249            eprintln!("Error reading directory {}: {}", dir_path.display(), e);
250            return;
251        }
252    };
253    
254    for entry_result in read_dir {
255        let entry = match entry_result {
256            Ok(e) => e,
257            Err(e) => {
258                eprintln!("Error reading directory entry: {}", e);
259                continue;
260            }
261        };
262        
263        let path = entry.path();
264        
265        if path.is_dir() {
266            // Recursively process subdirectory
267            collect_files_from_dir(&path, base_path, entries);
268        } else if path.is_file() {
269            // Add file to entries
270            add_file_to_entries(&path, base_path, entries);
271        }
272    }
273}
274
275// ----------------------------------------------------------------
276// simple methods for reading and writing tar archives
277// ----------------------------------------------------------------
278/// Packs files into a tar archive (supports .tar and .tar.gz)
279pub fn pack(tarfile: &str, files: &[&str]) {
280    let mut entries = Vec::new();
281    
282    for file_path in files {
283        let path = Path::new(file_path);
284        if !path.exists() {
285            eprintln!("Warning: File not found: {}", file_path);
286            continue;
287        }
288        
289        // Check if it's a directory
290        if path.is_dir() {
291            // Recursively add all files in the directory
292            collect_files_from_dir(path, path, &mut entries);
293        } else {
294            // Add single file - use parent directory as base to preserve filename
295            let base = path.parent().unwrap_or_else(|| Path::new(""));
296            add_file_to_entries(path, base, &mut entries);
297        }
298    }
299    
300    let tar_data = write_tar(&entries);
301    
302    // Compress if needed
303    let result = gzip(tarfile, tar_data)
304        .and_then(|data| fs::write(tarfile, data));
305    
306    match result {
307        Ok(_) => println!("Created tar archive: {}", tarfile),
308        Err(e) => {
309            eprintln!("Error writing tar file: {}", e);
310            std::process::exit(1);
311        }
312    }
313}
314
315/// Unpacks files from a tar archive (supports .tar and .tar.gz)
316pub fn unpack(tarfile: &str, output_dir: &str) {
317    unpack_with_options(tarfile, output_dir, false, true);
318}
319
320/// Unpacks a tar archive with options
321/// 
322/// # Arguments
323/// * `tarfile` - Path to the tar archive
324/// * `output_dir` - Output directory
325/// * `overwrite` - If true, overwrite existing files without prompting
326///                 If false, skip existing files
327/// * `use_prompt` - If true, prompt user for each existing file
328pub fn unpack_with_options(tarfile: &str, output_dir: &str, overwrite: bool, use_prompt: bool) {
329    let mut overwrite = overwrite;
330    // Read file
331    let file_data = match fs::read(tarfile) {
332        Ok(d) => d,
333        Err(e) => {
334            eprintln!("Error reading tar file: {}", e);
335            std::process::exit(1);
336        }
337    };
338    
339    // Decompress if gzipped
340    let tar_data = match ungzip(tarfile, file_data) {
341        Ok(data) => data,
342        Err(e) => {
343            eprintln!("Error decompressing gzip: {}", e);
344            std::process::exit(1);
345        }
346    };
347    
348    let entries = read_tar(&tar_data);
349    
350    let output_path = Path::new(output_dir);
351    if !output_path.exists() {
352        if let Err(e) = fs::create_dir_all(output_path) {
353            eprintln!("Error creating output directory: {}", e);
354            std::process::exit(1);
355        }
356    }
357    
358    for entry in entries {
359        let file_path = output_path.join(&entry.header.name);
360        let mut flag_overwrite = false;
361        // Check if file exists and overwrite is false
362        if file_path.exists() {
363            if !overwrite {
364                if use_prompt {
365                    // ask to user
366                    println!("❓File '{}' already exists. Overwrite? ([Y]es/[N]o/[A]ll): ", entry.header.name);
367                    let stdin = io::stdin();
368                    let mut line = String::new();
369                    stdin.lock().read_line(&mut line).unwrap_or(0);
370                    let answer = line.trim().to_lowercase();
371                    
372                    if answer == "a" || answer == "all" {
373                        // Overwrite this and all subsequent files
374                        println!("⚡ Overwriting all files...");
375                        overwrite = true;
376                    } else if answer == "y" || answer == "yes" {
377                    } else {
378                        println!("- Skipping: {}", entry.header.name);
379                        continue;
380                    }
381                } else {
382                    println!("- Skipping: {}", entry.header.name);
383                    continue;
384                }
385            }
386            flag_overwrite = true;
387        }
388        
389        // Create parent directories if they don't exist
390        if let Some(parent) = file_path.parent() {
391            if !parent.exists() {
392                if let Err(e) = fs::create_dir_all(parent) {
393                    eprintln!("❌ Error creating directory {}: {}", parent.display(), e);
394                    continue;
395                }
396            }
397        }
398        
399        match fs::File::create(&file_path) {
400            Ok(mut file) => {
401                if let Err(e) = file.write_all(&entry.data) {
402                    eprintln!("❌ Error writing {}: {}", entry.header.name, e);
403                } else {
404                    let overwrite_msg = if flag_overwrite { " (overwritten)" } else { "" };
405                    println!("- Extracted: {}{}", entry.header.name, overwrite_msg);
406                }
407            }
408            Err(e) => {
409                eprintln!("❌ Error creating {}: {}", entry.header.name, e);
410            }
411        }
412    }
413    
414    println!("Extraction complete to: {}", output_dir);
415}
416
417/// Lists TarHeader in a tar archive (supports .tar and .tar.gz)
418pub fn list(tarfile: &str) -> Result<Vec<TarHeader>, std::io::Error> {
419    let file_data = fs::read(tarfile)?;
420    
421    // Decompress if gzipped
422    let tar_data = ungzip(tarfile, file_data)?;
423    
424    let entries = read_tar(&tar_data);
425    let headers: Vec<TarHeader> = entries.into_iter().map(|e| e.header).collect();
426    Ok(headers)
427}
428
429/// Lists TarEntry in a tar archive (supports .tar and .tar.gz)
430pub fn list_entry(tarfile: &str) -> Result<Vec<TarEntry>, std::io::Error> {
431    let file_data = fs::read(tarfile)?;
432    
433    // Check if input is gzipped
434    let is_gzipped = tarfile.ends_with(".tar.gz") || tarfile.ends_with(".tgz");
435    
436    let tar_data = if is_gzipped {
437        // Decompress with gzip
438        let mut decoder = GzDecoder::new(&file_data[..]);
439        let mut decompressed = Vec::new();
440        decoder.read_to_end(&mut decompressed)?;
441        decompressed
442    } else {
443        file_data
444    };
445    
446    let entries = read_tar(&tar_data);
447    Ok(entries)
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453    use std::fs;
454    use std::path::Path;
455
456    #[test]
457    fn test_pack() {
458        // Create test files
459        let test_file1 = "test_file1.txt";
460        let test_file2 = "test_file2.txt";
461        let test_tar = "test_pack.tar";
462        
463        fs::write(test_file1, "Hello, World!").unwrap();
464        fs::write(test_file2, "Test content 2").unwrap();
465        
466        // Execute pack function
467        let files = vec![test_file1, test_file2];
468        pack(test_tar, &files);
469        
470        // Verify tar file was created
471        assert!(Path::new(test_tar).exists());
472        
473        // Verify tar file contents
474        let tar_data = fs::read(test_tar).unwrap();
475        let entries = read_tar(&tar_data);
476        assert_eq!(entries.len(), 2);
477        assert_eq!(entries[0].header.name, test_file1);
478        assert_eq!(entries[1].header.name, test_file2);
479        
480        // Cleanup
481        fs::remove_file(test_file1).unwrap();
482        fs::remove_file(test_file2).unwrap();
483        fs::remove_file(test_tar).unwrap();
484    }
485
486    #[test]
487    fn test_unpack() {
488        // Create test file and tar archive
489        let test_file = "test_unpack_file.txt";
490        let test_content = "Unpack test content";
491        let test_tar = "test_unpack.tar";
492        let output_dir = "test_unpack_output";
493        
494        fs::write(test_file, test_content).unwrap();
495        
496        // Create tar archive
497        let files = vec![test_file];
498        pack(test_tar, &files);
499        
500        // Execute unpack function
501        unpack_with_options(test_tar, output_dir, false, false);
502        
503        // Verify file was extracted
504        let extracted_file = Path::new(output_dir).join(test_file);
505        assert!(extracted_file.exists());
506        
507        // Verify file content
508        let content = fs::read_to_string(&extracted_file).unwrap();
509        assert_eq!(content, test_content);
510        
511        // Cleanup
512        fs::remove_file(test_file).unwrap();
513        fs::remove_file(test_tar).unwrap();
514        fs::remove_dir_all(output_dir).unwrap();
515    }
516
517    #[test]
518    fn test_list() {
519        // Create test files and tar archive
520        let test_file1 = "test_list_file1.txt";
521        let test_file2 = "test_list_file2.txt";
522        let test_tar = "test_list.tar";
523        
524        fs::write(test_file1, "Content 1").unwrap();
525        fs::write(test_file2, "Content 2 longer").unwrap();
526        
527        // Create tar archive
528        let files = vec![test_file1, test_file2];
529        pack(test_tar, &files);
530        
531        // Execute list function
532        let headers = list(test_tar).unwrap();
533        
534        // Verify results
535        assert_eq!(headers.len(), 2);
536        assert_eq!(headers[0].name, test_file1);
537        assert_eq!(headers[0].size, 9);
538        assert_eq!(headers[1].name, test_file2);
539        assert_eq!(headers[1].size, 16);
540        
541        // Verify tar file contents directly
542        let tar_data = fs::read(test_tar).unwrap();
543        let entries = read_tar(&tar_data);
544        assert_eq!(entries.len(), 2);
545        assert_eq!(entries[0].header.name, test_file1);
546        assert_eq!(entries[0].header.size, 9);
547        assert_eq!(entries[1].header.name, test_file2);
548        assert_eq!(entries[1].header.size, 16);
549        
550        // Cleanup
551        fs::remove_file(test_file1).unwrap();
552        fs::remove_file(test_file2).unwrap();
553        fs::remove_file(test_tar).unwrap();
554    }
555
556    #[test]
557    fn test_tar_gz() {
558        // Create test files
559        let test_file1 = "test_gz_file1.txt";
560        let test_file2 = "test_gz_file2.txt";
561        let test_tar_gz = "test_pack.tar.gz";
562        let output_dir = "test_gz_output";
563        
564        fs::write(test_file1, "GZ test content 1").unwrap();
565        fs::write(test_file2, "GZ test content 2 longer").unwrap();
566        
567        // Execute pack function (.tar.gz format)
568        let files = vec![test_file1, test_file2];
569        pack(test_tar_gz, &files);
570        
571        // Verify .tar.gz file was created
572        assert!(Path::new(test_tar_gz).exists());
573        
574        // Get file list with list function (from .tar.gz)
575        let headers = list(test_tar_gz).unwrap();
576        assert_eq!(headers.len(), 2);
577        assert_eq!(headers[0].name, test_file1);
578        assert_eq!(headers[0].size, 17);
579        assert_eq!(headers[1].name, test_file2);
580        assert_eq!(headers[1].size, 24);
581        
582        // Execute unpack function (extract from .tar.gz)
583        unpack_with_options(test_tar_gz, output_dir, false, false);
584        
585        // Verify files were extracted
586        let extracted_file1 = Path::new(output_dir).join(test_file1);
587        let extracted_file2 = Path::new(output_dir).join(test_file2);
588        assert!(extracted_file1.exists());
589        assert!(extracted_file2.exists());
590        
591        // Verify file contents
592        let content1 = fs::read_to_string(&extracted_file1).unwrap();
593        let content2 = fs::read_to_string(&extracted_file2).unwrap();
594        assert_eq!(content1, "GZ test content 1");
595        assert_eq!(content2, "GZ test content 2 longer");
596        
597        // Cleanup
598        fs::remove_file(test_file1).unwrap();
599        fs::remove_file(test_file2).unwrap();
600        fs::remove_file(test_tar_gz).unwrap();
601        fs::remove_dir_all(output_dir).unwrap();
602    }
603
604    #[test]
605    fn test_pack_directory() {
606        // Create test directory structure
607        let test_dir = "test_pack_dir";
608        let test_tar = "test_pack_dir.tar";
609        
610        fs::create_dir_all(format!("{}/subdir", test_dir)).unwrap();
611        fs::write(format!("{}/file1.txt", test_dir), "File 1 content").unwrap();
612        fs::write(format!("{}/file2.txt", test_dir), "File 2 content").unwrap();
613        fs::write(format!("{}/subdir/file3.txt", test_dir), "File 3 in subdir").unwrap();
614        
615        // Pack directory
616        let files = vec![test_dir];
617        pack(test_tar, &files);
618        
619        // Verify tar file was created
620        assert!(Path::new(test_tar).exists());
621        
622        // Verify tar file contents
623        let tar_data = fs::read(test_tar).unwrap();
624        let entries = read_tar(&tar_data);
625        assert_eq!(entries.len(), 3);
626        
627        // Verify file names (should be stored as relative paths)
628        let names: Vec<String> = entries.iter().map(|e| e.header.name.clone()).collect();
629        assert!(names.contains(&"file1.txt".to_string()));
630        assert!(names.contains(&"file2.txt".to_string()));
631        assert!(names.contains(&"subdir/file3.txt".to_string()));
632        
633        // Cleanup
634        fs::remove_dir_all(test_dir).unwrap();
635        fs::remove_file(test_tar).unwrap();
636    }
637
638    #[test]
639    fn test_pack_and_unpack_directory() {
640        // Create test directory structure
641        let test_dir = "test_dir_full";
642        let test_tar = "test_dir_full.tar";
643        let output_dir = "test_dir_full_output";
644        
645        fs::create_dir_all(format!("{}/a/b/c", test_dir)).unwrap();
646        fs::write(format!("{}/root.txt", test_dir), "Root file").unwrap();
647        fs::write(format!("{}/a/file_a.txt", test_dir), "File in a").unwrap();
648        fs::write(format!("{}/a/b/file_b.txt", test_dir), "File in b").unwrap();
649        fs::write(format!("{}/a/b/c/file_c.txt", test_dir), "File in c").unwrap();
650        
651        // Pack directory
652        let files = vec![test_dir];
653        pack(test_tar, &files);
654        
655        // unpack
656        unpack_with_options(test_tar, output_dir, false, false);
657        
658        // Verify all files were extracted
659        assert!(Path::new(output_dir).join("root.txt").exists());
660        assert!(Path::new(output_dir).join("a/file_a.txt").exists());
661        assert!(Path::new(output_dir).join("a/b/file_b.txt").exists());
662        assert!(Path::new(output_dir).join("a/b/c/file_c.txt").exists());
663        
664        // Verify file content
665        let content = fs::read_to_string(Path::new(output_dir).join("a/b/c/file_c.txt")).unwrap();
666        assert_eq!(content, "File in c");
667        
668        // Cleanup
669        fs::remove_dir_all(test_dir).unwrap();
670        fs::remove_file(test_tar).unwrap();
671        fs::remove_dir_all(output_dir).unwrap();
672    }
673
674    #[test]
675    fn test_pack_mixed_files_and_directories() {
676        // Create test file and directory
677        let test_file = "test_mixed_file.txt";
678        let test_dir = "test_mixed_dir";
679        let test_tar = "test_mixed.tar";
680        
681        fs::write(test_file, "Single file").unwrap();
682        fs::create_dir_all(format!("{}/subdir", test_dir)).unwrap();
683        fs::write(format!("{}/dir_file.txt", test_dir), "File in dir").unwrap();
684        fs::write(format!("{}/subdir/sub_file.txt", test_dir), "File in subdir").unwrap();
685        
686        // Pack mixed files and directories
687        let files = vec![test_file, test_dir];
688        pack(test_tar, &files);
689        
690        // Verify tar file contents
691        let tar_data = fs::read(test_tar).unwrap();
692        let entries = read_tar(&tar_data);
693        assert_eq!(entries.len(), 3);
694        
695        // Verify file names
696        let names: Vec<String> = entries.iter().map(|e| e.header.name.clone()).collect();
697        assert!(names.contains(&test_file.to_string()));
698        assert!(names.contains(&"dir_file.txt".to_string()));
699        assert!(names.contains(&"subdir/sub_file.txt".to_string()));
700        
701        // Cleanup
702        fs::remove_file(test_file).unwrap();
703        fs::remove_dir_all(test_dir).unwrap();
704        fs::remove_file(test_tar).unwrap();
705    }
706
707    #[test]
708    fn test_pack_directory_gzipped() {
709        // Create test directory structure
710        let test_dir = "test_pack_dir_gz";
711        let test_tar_gz = "test_pack_dir.tar.gz";
712        let output_dir = "test_pack_dir_gz_output";
713        
714        fs::create_dir_all(format!("{}/nested/deep", test_dir)).unwrap();
715        fs::write(format!("{}/file1.txt", test_dir), "First file").unwrap();
716        fs::write(format!("{}/nested/file2.txt", test_dir), "Second file").unwrap();
717        fs::write(format!("{}/nested/deep/file3.txt", test_dir), "Third file").unwrap();
718        
719        // Pack directory (gzip compressed)
720        let files = vec![test_dir];
721        pack(test_tar_gz, &files);
722        
723        // Verify .tar.gz file was created
724        assert!(Path::new(test_tar_gz).exists());
725        
726        // Verify contents with list
727        let headers = list(test_tar_gz).unwrap();
728        assert_eq!(headers.len(), 3);
729        
730        // Verify by unpacking
731        unpack_with_options(test_tar_gz, output_dir, false, false);
732        assert!(Path::new(output_dir).join("file1.txt").exists());
733        assert!(Path::new(output_dir).join("nested/file2.txt").exists());
734        assert!(Path::new(output_dir).join("nested/deep/file3.txt").exists());
735        
736        // Verify file content
737        let content = fs::read_to_string(Path::new(output_dir).join("nested/deep/file3.txt")).unwrap();
738        assert_eq!(content, "Third file");
739        
740        // Cleanup
741        fs::remove_dir_all(test_dir).unwrap();
742        fs::remove_file(test_tar_gz).unwrap();
743        fs::remove_dir_all(output_dir).unwrap();
744    }
745
746    #[test]
747    fn security_test_unpack_path_traversal() {
748        // Test that unpacking with path traversal attempts is handled
749        // Note: Current implementation is VULNERABLE - this test documents the risk
750        
751        use crate::tar::{TarEntry, TarHeader};
752        
753        let test_tar = "test_security_traversal.tar";
754        let output_dir = "test_security_output";
755        
756        // Create malicious tar with path traversal
757        let mut entries = Vec::new();
758        
759        // Attempt to write outside output directory
760        let malicious_paths = vec![
761            "../outside.txt",
762            "../../etc/outside2.txt",
763            "subdir/../../../outside3.txt",
764        ];
765        
766        for malicious_path in malicious_paths {
767            let header = TarHeader::new(malicious_path.to_string(), 0o644, 9);
768            let data = b"malicious".to_vec();
769            let header_bytes = header.to_bytes();
770            entries.push(TarEntry { header, data, header_bytes });
771        }
772        
773        let tar_data = write_tar(&entries);
774        fs::write(test_tar, tar_data).unwrap();
775        
776        // This WILL create files outside the intended directory (VULNERABILITY)
777        // In production, unpack should sanitize paths
778        unpack_with_options(test_tar, output_dir, false, false);
779        
780        // Cleanup
781        fs::remove_file(test_tar).unwrap();
782        if Path::new(output_dir).exists() {
783            fs::remove_dir_all(output_dir).ok();
784        }
785        // Also cleanup any files created outside (if they exist)
786        fs::remove_file("outside.txt").ok();
787        fs::remove_file("../outside.txt").ok();
788        fs::remove_file("outside2.txt").ok();
789        fs::remove_file("outside3.txt").ok();
790    }
791
792    #[test]
793    fn security_test_unpack_absolute_path() {
794        // Test handling of absolute paths in tar archives
795        // Note: Current implementation is VULNERABLE
796        
797        use crate::tar::{TarEntry, TarHeader};
798        
799        let test_tar = "test_security_absolute.tar";
800        let output_dir = "test_security_abs_output";
801        
802        // Create tar with absolute path (should be rejected or sanitized)
803        let header = TarHeader::new("/tmp/absolute_file.txt".to_string(), 0o644, 8);
804        let data = b"absolute".to_vec();
805        let header_bytes = header.to_bytes();
806        let entry = TarEntry { header, data, header_bytes };
807        
808        let tar_data = write_tar(&[entry]);
809        fs::write(test_tar, tar_data).unwrap();
810        
811        // This may write to /tmp/absolute_file.txt (VULNERABILITY)
812        unpack_with_options(test_tar, output_dir, false, false);
813        
814        // Cleanup
815        fs::remove_file(test_tar).unwrap();
816        if Path::new(output_dir).exists() {
817            fs::remove_dir_all(output_dir).ok();
818        }
819        // Cleanup absolute path file if created
820        fs::remove_file("/tmp/absolute_file.txt").ok();
821    }
822
823    #[test]
824    fn security_test_unpack_large_file_size() {
825        // Test handling of files with unrealistic size declarations
826        
827        use crate::tar::{TarEntry, TarHeader};
828        
829        let test_tar = "test_security_large.tar";
830        let output_dir = "test_security_large_output";
831        
832        // Create tar with exaggerated size but small actual data
833        let header = TarHeader::new("fake_large.txt".to_string(), 0o644, 5);
834        let data = b"small".to_vec();
835        let header_bytes = header.to_bytes();
836        let entry = TarEntry { header, data, header_bytes };
837        
838        let tar_data = write_tar(&[entry]);
839        fs::write(test_tar, tar_data).unwrap();
840        
841        // Should handle gracefully
842        unpack_with_options(test_tar, output_dir, false, false);
843        
844        // Verify file was created with actual (small) size
845        let extracted_file = Path::new(output_dir).join("fake_large.txt");
846        if extracted_file.exists() {
847            let content = fs::read(&extracted_file).unwrap();
848            assert_eq!(content.len(), 5);
849        }
850        
851        // Cleanup
852        fs::remove_file(test_tar).unwrap();
853        if Path::new(output_dir).exists() {
854            fs::remove_dir_all(output_dir).unwrap();
855        }
856    }
857
858    #[test]
859    fn security_test_unpack_empty_filename() {
860        // Test handling of entries with empty filenames
861        
862        use crate::tar::{TarEntry, TarHeader};
863        
864        let test_tar = "test_security_empty_name.tar";
865        let output_dir = "test_security_empty_output";
866        
867        // Create tar with empty filename
868        let header = TarHeader::new("".to_string(), 0o644, 4);
869        let data = b"data".to_vec();
870        let header_bytes = header.to_bytes();
871        let entry = TarEntry { header, data, header_bytes };
872        
873        let tar_data = write_tar(&[entry]);
874        fs::write(test_tar, tar_data).unwrap();
875        
876        // Should handle gracefully (may skip or error)
877        unpack_with_options(test_tar, output_dir, false, false);
878        
879        // Cleanup
880        fs::remove_file(test_tar).unwrap();
881        if Path::new(output_dir).exists() {
882            fs::remove_dir_all(output_dir).ok();
883        }
884    }
885
886    #[test]
887    fn security_test_unpack_special_characters() {
888        // Test handling of filenames with special characters
889        
890        use crate::tar::{TarEntry, TarHeader};
891        
892        let test_tar = "test_security_special.tar";
893        let output_dir = "test_security_special_output";
894        
895        // Create tar with special characters in filename
896        let special_names = vec![
897            "file\0with\0nulls.txt",
898            "file\nwith\nnewlines.txt",
899            "file;with;semicolons.txt",
900            "file|with|pipes.txt",
901        ];
902        
903        let mut entries = Vec::new();
904        for name in special_names {
905            let header = TarHeader::new(name.to_string(), 0o644, 7);
906            let data = b"special".to_vec();
907            let header_bytes = header.to_bytes();
908            entries.push(TarEntry { header, data, header_bytes });
909        }
910        
911        let tar_data = write_tar(&entries);
912        fs::write(test_tar, tar_data).unwrap();
913        
914        // Should handle gracefully
915        unpack_with_options(test_tar, output_dir, false, false);
916        
917        // Cleanup
918        fs::remove_file(test_tar).unwrap();
919        if Path::new(output_dir).exists() {
920            fs::remove_dir_all(output_dir).ok();
921        }
922    }
923
924    #[test]
925    fn security_test_pack_symlink_handling() {
926        // Test packing a directory that contains symlinks
927        // Should verify symlinks are handled appropriately
928        
929        #[cfg(unix)]
930        {
931            use std::os::unix::fs::symlink;
932            
933            let test_dir = "test_security_symlink_dir";
934            let test_tar = "test_security_symlink.tar";
935            
936            // Create directory with a symlink
937            fs::create_dir_all(test_dir).unwrap();
938            fs::write(format!("{}/target.txt", test_dir), "target content").unwrap();
939            
940            // Create symlink
941            let symlink_path = format!("{}/link.txt", test_dir);
942            let target_path = format!("{}/target.txt", test_dir);
943            symlink(&target_path, &symlink_path).ok(); // May fail on some systems
944            
945            // Pack directory
946            let files = vec![test_dir];
947            pack(test_tar, &files);
948            
949            // Verify tar was created
950            assert!(Path::new(test_tar).exists());
951            
952            // Cleanup
953            fs::remove_file(&symlink_path).ok();
954            fs::remove_dir_all(test_dir).unwrap();
955            fs::remove_file(test_tar).unwrap();
956        }
957    }
958
959    #[test]
960    fn security_test_unpack_overwrites_existing() {
961        // Test that unpacking overwrites existing files
962        // This could be a security concern if not properly documented
963        
964        use crate::tar::{TarEntry, TarHeader};
965        
966        let test_tar = "test_security_overwrite.tar";
967        let output_dir = "test_security_overwrite_output";
968        
969        fs::create_dir_all(output_dir).unwrap();
970        
971        // Create existing file with sensitive content
972        let sensitive_file = Path::new(output_dir).join("important.txt");
973        fs::write(&sensitive_file, "SENSITIVE DATA").unwrap();
974        
975        // Create tar that will overwrite it
976        let header = TarHeader::new("important.txt".to_string(), 0o644, 9);
977        let data = b"overwrite".to_vec();
978        let header_bytes = header.to_bytes();
979        let entry = TarEntry { header, data, header_bytes };
980        
981        let tar_data = write_tar(&[entry]);
982        fs::write(test_tar, tar_data).unwrap();
983        
984        // Unpack will overwrite existing file
985        unpack_with_options(test_tar, output_dir, true, false);
986        
987        // Verify file was overwritten
988        let content = fs::read_to_string(&sensitive_file).unwrap();
989        assert_eq!(content, "overwrite");
990        
991        // Cleanup
992        fs::remove_file(test_tar).unwrap();
993        fs::remove_dir_all(output_dir).unwrap();
994    }
995}