use crate::file::{DirEntry, File, Metadata, OpenOptions};
use crate::memory_fs::MemoryFS;
use crate::util::{not_supported, parent_iter};
use crate::FileSystem;
use std::io::{Read, Write};
use std::path::Path;
use tar::{Archive, EntryType};
pub struct TarFS {
memory_fs: MemoryFS,
}
pub trait FileSystemFilter {
fn should_include(&self, path: &Path) -> bool;
}
impl<F: Fn(&Path) -> bool> FileSystemFilter for F {
fn should_include(&self, path: &Path) -> bool {
self(path)
}
}
impl TarFS {
pub fn new<R: Read>(archive: R) -> crate::Result<Self> {
Self::new_filtered(archive, |_: &_| true)
}
pub fn new_filtered<R: Read, F: FileSystemFilter>(
archive: R,
filter: F,
) -> crate::Result<Self> {
let archive = Archive::new(archive);
Self::build_fs(archive, filter).map(|fs| Self { memory_fs: fs })
}
fn build_fs<R: Read, F: FileSystemFilter>(
mut archive: Archive<R>,
filter: F,
) -> crate::Result<MemoryFS> {
let memory_fs = MemoryFS::default();
for entry in archive.entries()? {
let mut entry = entry?;
if entry.header().entry_type() != EntryType::Regular {
continue;
}
let entry_path = entry.path()?.into_owned();
if !filter.should_include(&entry_path) {
continue;
}
for parent_path in parent_iter(&entry_path).map(Path::to_string_lossy).rev() {
if memory_fs.exists(&parent_path)? {
continue;
}
memory_fs.create_dir(&parent_path)?;
}
let mut file_contents = Vec::with_capacity(entry.header().size()? as usize);
entry.read_to_end(&mut file_contents)?;
let mut file = memory_fs.create_file(&format!("/{}", entry_path.to_string_lossy()))?;
file.write_all(&file_contents)?;
}
Ok(memory_fs)
}
}
impl FileSystem for TarFS {
fn create_dir(&self, _path: &str) -> crate::Result<()> {
Err(not_supported())
}
fn metadata(&self, path: &str) -> crate::Result<Metadata> {
self.memory_fs.metadata(path)
}
fn open_file_options(&self, path: &str, options: &OpenOptions) -> crate::Result<Box<dyn File>> {
if options.write {
return Err(not_supported());
}
self.memory_fs.open_file_options(path, options)
}
fn read_dir(
&self,
path: &str,
) -> crate::Result<Box<dyn Iterator<Item = crate::Result<DirEntry>>>> {
self.memory_fs.read_dir(path)
}
fn remove_dir(&self, _path: &str) -> crate::Result<()> {
Err(not_supported())
}
fn remove_file(&self, _path: &str) -> crate::Result<()> {
Err(not_supported())
}
}
#[cfg(test)]
mod test {
use std::fs::File;
use std::io::Read;
use crate::FileSystem;
use xz::read::XzDecoder;
use super::TarFS;
#[test]
fn bad_xz() {
let file = File::open("test/bad.tar.xz").unwrap();
let bad_archive = TarFS::new(XzDecoder::new(file));
assert!(bad_archive.is_err());
}
#[test]
fn single_file_xz_empty() {
let file = File::open("test/empty.tar.xz").unwrap();
let archive = TarFS::new(XzDecoder::new(file)).unwrap();
let files = archive.read_dir("").unwrap().collect::<Vec<_>>();
assert_eq!(files.len(), 1);
let mut empty_file = archive.open_file("/empty").unwrap();
let mut file_contents = vec![];
empty_file.read_to_end(&mut file_contents).unwrap();
assert_eq!(file_contents.len(), 0);
}
#[test]
fn single_file_xz_not_empty() {
let file = File::open("test/not_empty.tar.xz").unwrap();
let archive = TarFS::new(XzDecoder::new(file)).unwrap();
let files = archive.read_dir("").unwrap().collect::<Vec<_>>();
assert_eq!(files.len(), 1);
let mut file = archive.open_file("/not_empty").unwrap();
let mut file_contents = String::new();
file.read_to_string(&mut file_contents).unwrap();
assert_eq!(file_contents, "something interesting\n");
}
#[test]
fn deep_fs_xz() {
let file = File::open("test/deep_fs.tar.xz").unwrap();
let archive = TarFS::new(XzDecoder::new(file)).unwrap();
let files = archive.read_dir("folder").unwrap().collect::<Vec<_>>();
assert_eq!(files.len(), 2);
let mut file = archive.open_file("/folder/and/it/desc").unwrap();
let mut file_contents = String::new();
file.read_to_string(&mut file_contents).unwrap();
assert_eq!(file_contents, "it\n");
}
}