use std::{
collections::HashMap,
fs::File,
io::{self, BufReader, Write},
};
use anyhow::bail;
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
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: Utf8PathBuf,
pub path: Utf8PathBuf,
}
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(|s| Utf8Path::new(s).to_path_buf()))
.unwrap_or_else(|| abs_path.to_path_buf())
} else if abs_path.parent() == Some(&path_to_list)
|| abs_path.starts_with(&path_to_list)
{
let mut p = Utf8Path::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 {abs_path} while listing {path_to_list}"
);
};
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 '{path_to_list}' does not exist",)?;
write!(w, "{{}}")?;
} else {
bail!("path '{path_to_list}' does not exist");
}
}
Ok(())
}
fn print_listing<R, W: Write>(
w: &mut W,
paths_to_show: Vec<(Utf8PathBuf, 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} {rel_path} -> {target}", 0,)?;
} else {
writeln!(w, "{rel_path}")?;
}
}
Content::Directory => {
if long {
writeln!(w, "dr-xr-xr-x {:>20} {rel_path}", 0)?;
} else {
writeln!(w, "{rel_path}")?;
}
}
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}")?;
} else {
writeln!(w, "{rel_path}")?;
}
}
}
}
Ok(())
}
#[derive(Debug)]
enum JsonOutput {
Directory {
entries: HashMap<Utf8PathBuf, JsonOutput>,
},
Regular {
size: u64,
executable: bool,
nar_offset: u64,
},
Symlink {
target: Utf8PathBuf,
},
}
impl JsonOutput {
fn entries(&mut self) -> Result<&mut HashMap<Utf8PathBuf, 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: &str, other: Self) -> Result<&mut Self, anyhow::Error> {
let name = Utf8Path::new(frag).to_path_buf();
Ok(self.entries()?.entry(name).or_insert(other))
}
fn subdir(&mut self, frag: &str) -> 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<(Utf8PathBuf, 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 {
Utf8Component::CurDir => {
}
Utf8Component::Normal(frag) => {
cur_dir = cur_dir.subdir(frag)?;
}
comp @ (Utf8Component::Prefix(_)
| Utf8Component::RootDir
| Utf8Component::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.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.to_string()})
}
}
}