use std::{
collections::HashMap,
fs::File,
io::{self, BufReader, Write},
path::{Component, Path, PathBuf},
};
use anyhow::bail;
use clap::Parser;
use nix_nar::{Content, Decoder};
use serde_json::json;
#[derive(Parser)]
pub struct Opts {
#[clap(short, long)]
#[clap(long)]
pub json: bool,
#[clap(short, long)]
pub long: bool,
#[clap(short = 'R', long)]
pub recursive: bool,
pub nar: PathBuf,
pub path: PathBuf,
}
pub fn run<W: Write>(
mut w: W,
Opts {
json,
long,
recursive,
nar,
path: path_to_list,
}: Opts,
) -> Result<(), anyhow::Error> {
let file = BufReader::new(File::open(nar)?);
let dec = Decoder::new(file)?;
let mut printed_something = false;
let mut paths_to_show = vec![];
let mut selected_a_dir = false;
for entry in dec.entries()? {
let entry = entry?;
let abs_path = entry.abs_path();
let is_selected_dir = (abs_path == path_to_list) && entry.content.is_directory();
selected_a_dir = selected_a_dir || is_selected_dir;
if !is_selected_dir
&& (abs_path == path_to_list
|| abs_path.parent() == Some(&path_to_list)
|| (recursive && abs_path.starts_with(&path_to_list)))
{
printed_something = true;
let rel_path = if abs_path == path_to_list {
entry
.path
.as_ref()
.and_then(|p| p.file_name().map(Path::new))
.map(|p| p.to_path_buf())
.unwrap_or_else(|| abs_path.clone())
} else if abs_path.parent() == Some(&path_to_list)
|| abs_path.starts_with(&path_to_list)
{
let mut p = Path::new(".").to_path_buf();
p.push(abs_path.strip_prefix(&path_to_list)?);
p
} else {
bail!(
"Don't know how to construct relative path of {} while listing {}",
abs_path.display(),
path_to_list.display()
);
};
paths_to_show.push((rel_path, entry.content));
}
}
if json {
print_json(&mut w, paths_to_show, selected_a_dir, recursive)?;
} else {
print_listing(&mut w, paths_to_show, long)?;
}
if !printed_something {
if json {
writeln!(
io::stderr(),
"Error: path '{}' does not exist",
path_to_list.display()
)?;
write!(w, "{{}}")?;
} else {
bail!("path '{}' does not exist", path_to_list.display());
}
}
Ok(())
}
fn print_listing<R, W: Write>(
w: &mut W,
paths_to_show: Vec<(PathBuf, Content<R>)>,
long: bool,
) -> Result<(), anyhow::Error> {
for (rel_path, content) in paths_to_show {
match content {
Content::Symlink { target } => {
if long {
writeln!(
w,
"lrwxrwxrwx {:>20} {} -> {}",
0,
rel_path.display(),
target.display()
)?;
} else {
writeln!(w, "{}", rel_path.display())?;
}
}
Content::Directory => {
if long {
writeln!(w, "dr-xr-xr-x {:>20} {}", 0, rel_path.display())?;
} else {
writeln!(w, "{}", rel_path.display())?;
}
}
Content::File {
executable,
size,
offset: _,
data: _,
} => {
if long {
let perms = if executable {
"-r-xr-xr-x"
} else {
"-r--r--r--"
};
writeln!(w, "{perms} {size:>20} {}", rel_path.display())?;
} else {
writeln!(w, "{}", rel_path.display())?;
}
}
}
}
Ok(())
}
#[derive(Debug)]
enum JsonOutput {
Directory {
entries: HashMap<PathBuf, JsonOutput>,
},
Regular {
size: u64,
executable: bool,
nar_offset: u64,
},
Symlink {
target: PathBuf,
},
}
impl JsonOutput {
fn entries(&mut self) -> Result<&mut HashMap<PathBuf, Self>, anyhow::Error> {
match self {
Self::Regular { .. } => {
bail!("entries() called on Regular");
}
Self::Symlink { .. } => {
bail!("entries() called on Symlink");
}
Self::Directory { ref mut entries } => Ok(entries),
}
}
fn add_entry(
&mut self,
frag: &std::ffi::OsStr,
other: Self,
) -> Result<&mut Self, anyhow::Error> {
let name = Path::new(frag).to_path_buf();
Ok(self.entries()?.entry(name).or_insert(other))
}
fn subdir(&mut self, frag: &std::ffi::OsStr) -> Result<&mut Self, anyhow::Error> {
self.add_entry(frag, Self::new_dir())
}
fn new_dir() -> Self {
JsonOutput::Directory {
entries: HashMap::new(),
}
}
}
fn print_json<R, W: Write>(
w: &mut W,
paths_to_show: Vec<(PathBuf, Content<R>)>,
selected_a_dir: bool,
recursive: bool,
) -> Result<(), anyhow::Error> {
let mut res: JsonOutput;
if paths_to_show.is_empty() {
return Ok(());
} else if paths_to_show.len() == 1 {
let file = match paths_to_show[0] {
(
_,
Content::File {
ref size,
ref executable,
ref offset,
..
},
) => JsonOutput::Regular {
size: *size,
executable: *executable,
nar_offset: *offset,
},
(_, Content::Symlink { ref target }) => JsonOutput::Symlink {
target: target.clone(),
},
(_, Content::Directory) => {
JsonOutput::new_dir()
}
};
if selected_a_dir {
res = JsonOutput::new_dir();
res.add_entry(paths_to_show[0].0.file_name().unwrap(), file)?;
} else {
res = file;
}
} else {
res = JsonOutput::new_dir();
for (path, content) in paths_to_show {
let mut cur_dir = &mut res;
for c in path.parent().unwrap().components() {
match c {
Component::CurDir => {
}
Component::Normal(frag) => {
cur_dir = cur_dir.subdir(frag)?;
}
comp @ (Component::Prefix(_)
| Component::RootDir
| Component::ParentDir) => {
bail!("unexpected path component {:?}", comp)
}
}
}
let filename = path.file_name().unwrap();
match content {
Content::Directory => {
cur_dir.subdir(filename)?;
}
Content::File {
ref size,
ref executable,
ref offset,
..
} => {
cur_dir.add_entry(
filename,
JsonOutput::Regular {
size: *size,
executable: *executable,
nar_offset: *offset,
},
)?;
}
Content::Symlink { ref target } => {
cur_dir.add_entry(
filename,
JsonOutput::Symlink {
target: target.clone(),
},
)?;
}
}
}
}
serde_json::to_writer(w, &convert_to_json(&res, recursive))?;
Ok(())
}
fn convert_to_json(json_output: &JsonOutput, recursive: bool) -> serde_json::Value {
match json_output {
JsonOutput::Directory { entries } => {
let entries: serde_json::Map<String, serde_json::Value> = entries
.iter()
.map(|(p, j)| {
(
p.display().to_string(),
if recursive {
convert_to_json(j, recursive)
} else {
json!({})
},
)
})
.collect();
json!({ "type": "directory", "entries": entries })
}
JsonOutput::Regular {
size,
nar_offset,
executable,
} => {
if *executable {
json!({ "type": "regular", "size": size, "narOffset": nar_offset, "executable": true})
} else {
json!({ "type": "regular", "size": size, "narOffset": nar_offset})
}
}
JsonOutput::Symlink { target } => {
json!({"type": "symlink", "target": target.display().to_string()})
}
}
}