use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use clap::Args;
use libsail::collection::{Indexable, Iterable};
use libsail::index::Reader;
use crate::cli::{FormatArg, Order, ReadArgs};
use crate::input::{Backend, Inputs, Needs, dispatch};
use crate::output::{emit, put, writer};
#[derive(Args)]
pub struct FetchArgs {
pub input: PathBuf,
pub names: Vec<String>,
#[arg(short = 'f', long)]
pub names_file: Option<PathBuf>,
#[arg(long, value_enum)]
pub format: Option<FormatArg>,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub skip_missing: bool,
#[arg(long, value_enum, default_value_t)]
pub order: Order,
#[arg(long)]
pub rewrap: bool,
#[command(flatten)]
pub read: ReadArgs,
}
impl FetchArgs {
pub fn run(self) -> Result<()> {
let wanted = self.wanted()?;
if wanted.is_empty() {
bail!("fetch needs at least one name, on the command line or through -f");
}
let inputs = Inputs::plan(
std::slice::from_ref(&self.input),
self.format,
self.read,
Needs::Pass,
)?;
let entry = &inputs.entries()[0];
let mut out = writer(self.output.as_deref())?;
let skip_missing = self.skip_missing;
if inputs.backend() != Backend::Memory {
let format = inputs.format();
let want: HashSet<&[u8]> = wanted.iter().map(Vec::as_slice).collect();
let mut found: HashMap<&[u8], Vec<u8>> = HashMap::new();
let mut hit: HashSet<&[u8]> = HashSet::with_capacity(want.len());
let mut reader = Reader::new(entry.reader()?, format);
while reader.advance()? {
let record = reader.record();
let Some(name) = libsail::seq::name_of(format, record) else {
continue;
};
let Some(&asked) = want.get(name) else {
continue;
};
if !hit.insert(asked) {
continue;
}
match self.order {
Order::File => put(format, record, self.rewrap, &mut out)?,
Order::Asked => {
found.insert(asked, record.to_vec());
}
}
}
for name in &wanted {
if hit.contains(name.as_slice()) {
continue;
}
if !skip_missing {
bail!(
"{} holds no record named {:?}",
entry.name(),
String::from_utf8_lossy(name)
)
}
}
if self.order == Order::Asked {
for name in &wanted {
if let Some(record) = found.get(name.as_slice()) {
put(format, record, self.rewrap, &mut out)?;
}
}
}
out.flush()?;
return Ok(());
}
dispatch!(inputs.format(), entry, |collection, _size, name, write| {
let mut at: HashMap<&[u8], usize> = HashMap::with_capacity(collection.len());
for (n, record) in collection.iter().enumerate() {
if let Some(found) = name(record) {
at.entry(found).or_insert(n);
}
}
let mut picked = Vec::with_capacity(wanted.len());
for want in &wanted {
match at.get(want.as_slice()) {
Some(&n) => picked.push(
collection
.cloned(n)
.expect("a position below len() is a record"),
),
None if skip_missing => {}
None => bail!(
"{} holds no record named {:?}",
entry.name(),
String::from_utf8_lossy(want)
),
}
}
emit(picked, write, &mut out)
})
}
fn wanted(&self) -> Result<Vec<Vec<u8>>> {
let mut wanted: Vec<Vec<u8>> = self.names.iter().map(|n| n.as_bytes().to_vec()).collect();
let Some(path) = &self.names_file else {
return Ok(wanted);
};
let bytes = std::fs::read(path)
.with_context(|| format!("reading the names in {}", path.display()))?;
wanted.extend(
bytes
.split(|&b| b == b'\n')
.map(|line| line.strip_suffix(b"\r").unwrap_or(line))
.filter(|line| !line.is_empty())
.map(<[u8]>::to_vec),
);
Ok(wanted)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn args() -> FetchArgs {
FetchArgs {
input: PathBuf::new(),
names: Vec::new(),
names_file: None,
format: None,
output: None,
skip_missing: false,
order: Order::default(),
rewrap: false,
read: ReadArgs::default(),
}
}
#[test]
fn names_come_from_the_command_line_and_then_from_the_file() {
let path = std::env::temp_dir().join(format!("sail-fw-{}.txt", std::process::id()));
std::fs::write(&path, b"PDZ\nSH3_1\n").unwrap();
let fetch = FetchArgs {
names: vec!["first".to_string()],
names_file: Some(path.clone()),
..args()
};
assert_eq!(
fetch.wanted().unwrap(),
[&b"first"[..], b"PDZ", b"SH3_1"].map(<[u8]>::to_vec)
);
std::fs::remove_file(path).ok();
}
#[test]
fn a_name_file_drops_blank_lines_and_a_trailing_newline() {
let path = std::env::temp_dir().join(format!("sail-fb-{}.txt", std::process::id()));
std::fs::write(&path, b"PDZ\n\r\n\nSH3_1\r\n").unwrap();
let fetch = FetchArgs {
names_file: Some(path.clone()),
..args()
};
assert_eq!(
fetch.wanted().unwrap(),
[&b"PDZ"[..], b"SH3_1"].map(<[u8]>::to_vec)
);
std::fs::remove_file(path).ok();
}
}