use assert_matches::assert_matches;
use ignore::Walk;
use insta::assert_snapshot;
use nix_nar::{debug::pretty_print_nar_content, *};
use std::{
fs::{self, File, Metadata},
io::{BufReader, Read},
};
#[test]
fn invalid_archive() {
let input = "not a nar archive".as_bytes();
assert!(
Decoder::new(input).is_err(),
"the parser didn't reject a stream without the header"
);
}
#[test]
fn parse_empty_file_low_level() {
let input = include_bytes!("../test-data/02-empty-file.nar");
assert_eq!(input.len(), 112);
let dec = Decoder::new(&input[..]).unwrap();
let mut entries = dec.entries().unwrap();
let file_entry = entries.next();
assert_matches!(
file_entry,
Some(Ok(Entry {
path: None,
content: Content::File {
executable: false,
size: 0,
offset: 96,
data: _,
}
}))
);
if let Some(Ok(Entry {
content: Content::File { mut data, .. },
..
})) = file_entry
{
let mut str = String::new();
data.read_to_string(&mut str).unwrap();
assert_eq!(str, "");
}
assert_matches!(entries.next(), None);
}
#[test]
fn parse_emtpy_dir() {
let input = include_bytes!("../test-data/01-empty-dir.nar");
assert_eq!(input.len(), 96);
assert_snapshot!(pretty_print_nar_content(Decoder::new(&input[..]).unwrap()), @"ROOT");
}
#[test]
fn parse_empty_file() {
let input = include_bytes!("../test-data/02-empty-file.nar");
assert_eq!(input.len(), 112);
assert_snapshot!(
pretty_print_nar_content(Decoder::new(&input[..]).unwrap()),
@"ROOT: executable=false, size=0, offset=96, data=''"
);
}
#[test]
fn parse_dir_one_empty_file() {
let input = include_bytes!("../test-data/03-dir-one-empty-file.nar");
assert_eq!(input.len(), 288);
assert_snapshot!(pretty_print_nar_content(Decoder::new(&input[..]).unwrap()), @r###"
ROOT
├── an-empty-file: executable=false, size=0, offset=240, data=''
"###);
}
#[test]
fn parse_small_file() {
let input = include_bytes!("../test-data/04-small-file.nar");
assert_eq!(input.len(), 136);
assert_snapshot!(
pretty_print_nar_content(Decoder::new(&input[..]).unwrap()),
@r###"ROOT: executable=false, size=21, offset=96, data='This is a test file.\n'"###
);
}
#[test]
fn parse_executable_file() {
let input = include_bytes!("../test-data/05-executable-file.in.exe.nar");
assert_eq!(input.len(), 144);
assert_snapshot!(
pretty_print_nar_content(Decoder::new(&input[..]).unwrap()),
@"ROOT: executable=true, size=0, offset=128, data=''"
);
}
#[test]
fn parse_symlink() {
let input = include_bytes!("../test-data/06-symlink.nar");
assert_eq!(input.len(), 128);
assert_snapshot!(
pretty_print_nar_content(Decoder::new(&input[..]).unwrap()),
@"ROOT -> 02-empty-file.in"
);
}
#[test]
fn parse_nested_dirs() {
let input = include_bytes!("../test-data/07-nested-dirs.nar");
assert_eq!(input.len(), 1504);
assert_snapshot!(pretty_print_nar_content(Decoder::new(&input[..]).unwrap()), @r###"
ROOT
├── 01-an-empty-file: executable=false, size=0, offset=240, data=''
├── 02-some-dir
│ ├── link-to-an-empty-file -> ../01-an-empty-file
│ ├── more-depth
│ │ ├── deep-empty-file: executable=false, size=0, offset=944, data=''
│ ├── small-file: executable=false, size=21, offset=1168, data='This is a test file.\n'
├── 03-executable-file.exe: executable=true, size=0, offset=1456, data=''
"###);
}
#[test]
fn parse_nested_dirs_from_file() {
let file = File::open("test-data/07-nested-dirs.nar").unwrap();
assert_snapshot!(pretty_print_nar_content(Decoder::new(file).unwrap()), @r###"
ROOT
├── 01-an-empty-file: executable=false, size=0, offset=240, data=''
├── 02-some-dir
│ ├── link-to-an-empty-file -> ../01-an-empty-file
│ ├── more-depth
│ │ ├── deep-empty-file: executable=false, size=0, offset=944, data=''
│ ├── small-file: executable=false, size=21, offset=1168, data='This is a test file.\n'
├── 03-executable-file.exe: executable=true, size=0, offset=1456, data=''
"###);
}
#[test]
fn parse_nested_dirs_from_bufreader() {
let file = BufReader::new(File::open("test-data/07-nested-dirs.nar").unwrap());
assert_snapshot!(pretty_print_nar_content(Decoder::new(file).unwrap()), @r###"
ROOT
├── 01-an-empty-file: executable=false, size=0, offset=240, data=''
├── 02-some-dir
│ ├── link-to-an-empty-file -> ../01-an-empty-file
│ ├── more-depth
│ │ ├── deep-empty-file: executable=false, size=0, offset=944, data=''
│ ├── small-file: executable=false, size=21, offset=1168, data='This is a test file.\n'
├── 03-executable-file.exe: executable=true, size=0, offset=1456, data=''
"###);
}
#[cfg(target_family = "unix")]
fn get_mode(m: &Metadata) -> u32 {
use std::os::unix::fs::PermissionsExt;
m.permissions().mode()
}
#[cfg(target_family = "windows")]
fn get_mode(m: &Metadata) -> u32 {
0
}
#[test]
#[cfg(target_family = "unix")]
fn unpack_unix() -> Result<(), anyhow::Error> {
let file = BufReader::new(File::open("test-data/07-nested-dirs.nar").unwrap());
let temp_dir = tempfile::tempdir()?;
Decoder::new(file)?.unpack(temp_dir.path().join("unpacked"))?;
let mut res = vec![];
for entry in Walk::new(temp_dir.path()) {
match entry {
Ok(entry) => {
let path = entry.path();
let meta = {
let metadata = fs::symlink_metadata(path)?;
if metadata.is_dir() {
"DIR".to_string()
} else if metadata.is_symlink() {
let target = fs::read_link(path)?;
format!("-> {}", target.display())
} else if metadata.is_file() {
format!(
" FILE: size: {}, mode: 0o{:o}",
metadata.len(),
get_mode(&metadata)
)
} else {
"UNKNOWN FILE TYPE".to_string()
}
};
res.push(format!(
"{} {meta}",
entry.path().strip_prefix(temp_dir.path())?.display()
))
}
Err(err) => res.push(format!("ERROR: {}", err)),
}
}
res.sort();
assert_snapshot!(res.join("\n"), @r###"
DIR
unpacked DIR
unpacked/01-an-empty-file FILE: size: 0, mode: 0o100444
unpacked/02-some-dir DIR
unpacked/02-some-dir/link-to-an-empty-file -> ../01-an-empty-file
unpacked/02-some-dir/more-depth DIR
unpacked/02-some-dir/more-depth/deep-empty-file FILE: size: 0, mode: 0o100444
unpacked/02-some-dir/small-file FILE: size: 21, mode: 0o100444
unpacked/03-executable-file.exe FILE: size: 0, mode: 0o100555
"###
);
Ok(())
}
#[test]
#[cfg(target_family = "windows")]
fn unpack_windows() -> Result<(), anyhow::Error> {
let file =
BufReader::new(File::open("test-data/08-nested-dirs-no-symlinks.nar").unwrap());
let temp_dir = tempfile::tempdir()?;
Decoder::new(file)?.unpack(&temp_dir.path().join("unpacked"))?;
let mut res = vec![];
for entry in Walk::new(&temp_dir.path()) {
match entry {
Ok(entry) => {
let path = entry.path();
let meta = {
let metadata = fs::symlink_metadata(path)?;
if metadata.is_dir() {
"DIR".to_string()
} else if metadata.is_symlink() {
let target = fs::read_link(path)?;
format!("-> {}", target.display())
} else if metadata.is_file() {
format!(
" FILE: size: {}, mode: 0o{:o}",
metadata.len(),
get_mode(&metadata)
)
} else {
"UNKNOWN FILE TYPE".to_string()
}
};
res.push(format!(
"{} {meta}",
entry.path().strip_prefix(&temp_dir.path())?.display()
))
}
Err(err) => res.push(format!("ERROR: {}", err)),
}
}
res.sort();
assert_snapshot!(res.join("\n"), @r###"
DIR
unpacked DIR
unpacked\01-an-empty-file FILE: size: 0, mode: 0o0
unpacked\02-some-dir DIR
unpacked\02-some-dir\more-depth DIR
unpacked\02-some-dir\more-depth\deep-empty-file FILE: size: 0, mode: 0o0
unpacked\02-some-dir\small-file FILE: size: 21, mode: 0o0
unpacked\03-executable-file.exe FILE: size: 0, mode: 0o0
"###
);
Ok(())
}