use anyhow::{Result, Context};
use clap::{Parser, CommandFactory};
use clap::error::ErrorKind;
use memchr::{memchr, memmem};
use memmap2::Mmap;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use rustc_hash::{FxHashMap, FxHashSet};
use std::{
fs::File,
io::{BufWriter, IoSlice, Write, stdout},
path::{Path, PathBuf},
str,
};
const MISSING: u64 = u64::MAX;
#[derive(Debug, Clone, Parser)]
pub struct CommonArgs {
#[arg(short = 'i', long = "input", value_name = "FILE")]
pub input: PathBuf,
#[arg(short = 'o', long = "output", value_name = "FILE")]
pub output: Option<PathBuf>,
#[arg(short = 'e', long = "entire_group", default_value_t = false)]
pub entire_group: bool,
#[arg(short = 'T', long = "types", value_name = "TYPES")]
pub types: Option<String>,
#[arg(
short = 't',
long = "threads",
default_value_t = 12,
value_name = "NUM",
)]
pub threads: usize,
#[arg(
short = 'v',
long = "verbose",
default_value_t = false,
value_name = "BOOL",
)]
pub verbose: bool,
}
impl CommonArgs {
#[inline]
pub fn effective_threads(&self) -> usize {
if self.threads == 0 {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
} else {
self.threads
}
}
pub fn init_rayon(&self) {
let n = self.effective_threads();
match rayon::ThreadPoolBuilder::new().num_threads(n).build_global() {
Ok(()) => {
if self.verbose {
eprintln!("[INFO] rayon threads = {}", n);
}
}
Err(e) => {
if self.verbose {
eprintln!("[WARN] rayon global pool already initialized: {e}");
}
}
}
}
pub fn post_parse(&self) -> Result<(), clap::Error> {
if self.entire_group && self.types.is_some() {
return Err(
clap::Error::raw(
ErrorKind::ArgumentConflict,
"Full-model mode does not support filtering by feature types (-T/--types).",
)
.with_cmd(&Self::command())
);
}
self.init_rayon();
Ok(())
}
pub fn parse_and_init() -> Self {
let args = Self::parse();
if let Err(e) = args.post_parse() {
e.exit();
}
args
}
}
pub fn append_suffix(path: &Path, suffix: &str) -> PathBuf {
let parent = path.parent().unwrap_or_else(|| Path::new(""));
let filename = path.file_name().unwrap_or_default().to_string_lossy();
parent.join(format!("{filename}{suffix}"))
}
pub fn write_gff_header<W: Write>(writer: &mut W, gff_buf: &[u8]) -> Result<usize> {
let mut pos = 0;
while pos < gff_buf.len() && gff_buf[pos] == b'#' {
if let Some(nl) = gff_buf[pos..].iter().position(|&b| b == b'\n') {
let end = pos + nl + 1;
writer.write_all(&gff_buf[pos..end])?;
pos = end;
} else {
break;
}
}
Ok(pos)
}
pub fn check_index_files_exist(gff: &PathBuf) -> Result<bool> {
let expected_suffixes = [
".gof", ".fts", ".prt", ".sqs", ".atn", ".a2f", ".rit", ".rix",
];
let mut missing = Vec::new();
for ext in &expected_suffixes {
let path = append_suffix(gff, ext);
if !path.exists() {
missing.push(ext.to_string());
}
}
if !missing.is_empty() {
eprintln!("Missing index file(s): {:?}", missing);
Ok(false)
} else {
Ok(true)
}
}
pub fn write_gff_output(
gff_path: &Path,
blocks: &[(u32, u64, u64)],
output_path: &Option<std::path::PathBuf>,
verbose: bool,
) -> Result<()> {
let file = File::open(gff_path)?;
let mmap = unsafe { Mmap::map(&file)? };
let file_len = mmap.len();
let mut sorted: Vec<(u64, u64)> = {
let mut v = Vec::with_capacity(blocks.len());
for &(fid, s, e) in blocks {
if s == MISSING {
eprintln!("[WARN] skipped fid={} due to sentinel start offset", fid);
continue;
}
v.push((s, e));
}
v
};
sorted.sort_unstable_by_key(|&(s, _)| s);
let mut merged: Vec<(u64, u64)> = Vec::with_capacity(sorted.len());
let mut it = sorted.into_iter();
if let Some((mut cs, mut ce)) = it.next() {
for (s, e) in it {
if s <= ce {
ce = ce.max(e);
} else {
if cs < ce {
merged.push((cs, ce));
}
cs = s;
ce = e;
}
}
if cs < ce {
merged.push((cs, ce));
}
}
let mut slices: Vec<IoSlice<'_>> = Vec::with_capacity(merged.len());
for &(so, eo) in &merged {
if so >= eo {
continue;
}
let (start, end) = (so as usize, eo as usize);
if end > file_len {
continue;
}
slices.push(IoSlice::new(&mmap[start..end]));
}
let mut writer: Box<dyn Write> = match output_path {
Some(p) => Box::new(BufWriter::new(File::create(p)?)),
None => Box::new(BufWriter::new(stdout())),
};
const MAX_IOV: usize = 1024;
let mut base = 0;
while base < slices.len() {
let end = (base + MAX_IOV).min(slices.len());
let batch = &slices[base..end];
let nw = writer.write_vectored(batch)?;
let mut remaining = nw;
let mut i = 0;
while i < batch.len() && remaining >= batch[i].len() {
remaining -= batch[i].len();
i += 1;
}
if i < batch.len() && remaining > 0 {
let cur = &batch[i];
writer.write_all(&cur[remaining..])?;
i += 1;
}
for s in &batch[i..] {
writer.write_all(s)?;
}
base = end;
}
writer.flush()?;
if verbose {
eprintln!(
"Wrote {} merged GFF block(s) with vectored I/O",
merged.len()
);
}
Ok(())
}
pub fn write_gff_output_filtered(
gff_path: &PathBuf,
blocks: &[(u32, u64, u64)],
per_root_matches: &FxHashMap<u32, FxHashSet<String>>,
atn_attr_name: &str,
output_path: &Option<PathBuf>,
types_filter: Option<&str>,
verbose: bool,
) -> Result<()> {
let file =
File::open(gff_path).with_context(|| format!("Cannot open GFF file: {:?}", gff_path))?;
let mmap =
unsafe { Mmap::map(&file) }.with_context(|| format!("mmap failed for {:?}", gff_path))?;
let file_len = mmap.len();
let type_allow: Option<FxHashSet<String>> = types_filter.map(|s| {
s.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect()
});
let bkey: Vec<u8> = {
let mut k = atn_attr_name.as_bytes().to_vec();
k.push(b'=');
k
};
let bkey_finder = memmem::Finder::new(&bkey);
let mut parts: Vec<(u64, Vec<u8>)> = blocks
.par_iter()
.filter_map(|&(root, start, end)| {
let keep: &FxHashSet<String> = per_root_matches.get(&root)?;
if keep.is_empty() {
return None;
}
let s = start as usize;
let e = end.min(file_len as u64) as usize;
if s >= e || e > file_len {
return None;
}
let window = &mmap[s..e];
let mut out = Vec::<u8>::with_capacity(1024);
let mut pos = 0usize;
let next_line = |from: usize| -> Option<(usize, usize, usize)> {
if from >= window.len() {
return None;
}
let rel = memchr(b'\n', &window[from..])
.map(|i| from + i + 1)
.unwrap_or(window.len());
let mut end_no_nl = rel;
if end_no_nl > from && window[end_no_nl - 1] == b'\n' {
end_no_nl -= 1;
}
if end_no_nl > from && window[end_no_nl - 1] == b'\r' {
end_no_nl -= 1;
}
Some((from, rel, end_no_nl))
};
let type_ok = |line: &[u8]| -> bool {
if let Some(allow) = &type_allow {
let i1 = match memchr(b'\t', line) {
Some(i) => i,
None => return false,
};
let i2 = match memchr(b'\t', &line[i1 + 1..]) {
Some(x) => i1 + 1 + x,
None => return false,
};
let i3 = match memchr(b'\t', &line[i2 + 1..]) {
Some(x) => i2 + 1 + x,
None => return false,
};
let ty = &line[i2 + 1..i3];
if let Ok(ty_str) = std::str::from_utf8(ty) {
allow.contains(ty_str)
} else {
false
}
} else {
true
}
};
let id_hits_keep = |line_no_crlf: &[u8]| -> bool {
let mut off = 0usize;
let mut tabs = 0u8;
while tabs < 8 {
match memchr(b'\t', &line_no_crlf[off..]) {
Some(i) => {
off += i + 1;
tabs += 1;
}
None => return false,
}
}
let attr = &line_no_crlf[off..];
if let Some(p) = bkey_finder.find(attr) {
let vstart = p + bkey.len();
let vend = memchr(b';', &attr[vstart..])
.map(|i| vstart + i)
.unwrap_or(attr.len());
let id_slice = &attr[vstart..vend];
if let Ok(id_str) = std::str::from_utf8(id_slice) {
return keep.contains(id_str);
}
}
false
};
while let Some((ls, le, ln_end)) = next_line(pos) {
pos = le;
let line = &window[ls..le];
if !line.is_empty() && line[0] == b'#' {
continue; }
let line_no_crlf = &window[ls..ln_end];
if !type_ok(line_no_crlf) {
continue;
}
if id_hits_keep(line_no_crlf) {
out.extend_from_slice(line);
}
}
if verbose {
let matched_lines = out.iter().filter(|&&b| b == b'\n').count();
eprintln!(
"[filter] root={} block=[{}..{}] keep_ids={} matched_lines={}",
root, start, end, keep.len(), matched_lines
);
}
if out.is_empty() {
None
} else {
Some((start, out))
}
})
.collect();
parts.sort_unstable_by_key(|(s, _)| *s);
let raw: Box<dyn Write> = match output_path {
Some(p) => Box::new(File::create(p).with_context(|| format!("Cannot create output: {:?}", p))?),
None => Box::new(std::io::stdout()),
};
let mut writer = BufWriter::with_capacity(16 * 1024 * 1024, raw);
for (_, buf) in parts {
writer.write_all(&buf)?;
}
writer.flush()?;
Ok(())
}