use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Seek, Write};
use std::path::Path;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use grep;
use grep::matcher::{LineMatchKind, Match, Matcher, NoError};
use memchr::{memchr, memrchr};
use regex::bytes::Regex;
use regex_syntax::ast::{AssertionKind, Ast, Literal};
use serde_json;
use thiserror::Error;
use zstd;
use crate::files::{FileTree, FileTreeEntry};
use crate::frcode;
use crate::package::StorePath;
const FORMAT_VERSION: u64 = 1;
const FILE_MAGIC: &[u8] = b"NIXI";
pub struct Writer {
writer: Option<BufWriter<zstd::Encoder<'static, File>>>,
}
impl Drop for Writer {
fn drop(&mut self) {
if self.writer.is_some() {
self.finish_encoder().expect("failed to flush database");
}
}
}
impl Writer {
pub fn create<P: AsRef<Path>>(path: P, level: i32) -> io::Result<Writer> {
let mut file = File::create(path)?;
file.write_all(FILE_MAGIC)?;
file.write_u64::<LittleEndian>(FORMAT_VERSION)?;
let mut encoder = zstd::Encoder::new(file, level)?;
encoder.multithread(num_cpus::get() as u32)?;
Ok(Writer {
writer: Some(BufWriter::new(encoder)),
})
}
pub fn add(
&mut self,
path: StorePath,
files: FileTree,
filter_prefix: &[u8],
) -> io::Result<()> {
let entries = files.to_list(filter_prefix);
if entries.is_empty() {
return Ok(());
}
let writer = self.writer.as_mut().expect("not dropped yet");
let mut encoder = frcode::Encoder::new(
writer,
b"p".to_vec(),
serde_json::to_vec(&path).expect("failed to serialize path"),
);
for entry in entries {
entry.encode(&mut encoder)?;
}
Ok(())
}
fn finish_encoder(&mut self) -> io::Result<File> {
let writer = self.writer.take().expect("not dropped yet");
let encoder = writer.into_inner()?;
encoder.finish()
}
pub fn finish(mut self) -> io::Result<u64> {
let mut file = self.finish_encoder()?;
file.stream_position()
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("expected file to start with nix-index file magic 'NIXI', but found '{found:?}' (is this a valid nix-index database file?)")]
UnsupportedFileType { found: Vec<u8> },
#[error("this executable only supports the nix-index database version {}, but found a database with version {found}", FORMAT_VERSION)]
UnsupportedVersion { found: u64 },
#[error("database corrupt, found a file entry without a matching package entry")]
MissingPackageEntry,
#[error("database corrupt, frcode error: {0}")]
Frcode(#[from] frcode::Error),
#[error("database corrupt, could not parse entry: {entry:?}")]
EntryParse { entry: Vec<u8> },
#[error("database corrupt, could not parse store path: {path:?}")]
StorePathParse { path: Vec<u8> },
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("grep error: {0}")]
Grep(#[from] grep::regex::Error),
}
type Result<T> = std::result::Result<T, Error>;
pub struct Reader {
decoder: frcode::Decoder<BufReader<zstd::Decoder<'static, BufReader<File>>>>,
}
impl Reader {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Reader> {
let mut file = File::open(path)?;
let mut magic = [0u8; 4];
file.read_exact(&mut magic)?;
if magic != FILE_MAGIC {
return Err(Error::UnsupportedFileType {
found: magic.to_vec(),
});
}
let version = file.read_u64::<LittleEndian>()?;
if version != FORMAT_VERSION {
return Err(Error::UnsupportedVersion { found: version });
}
let decoder = zstd::Decoder::new(file)?;
Ok(Reader {
decoder: frcode::Decoder::new(BufReader::new(decoder)),
})
}
pub fn query(self, exact_regex: &Regex) -> Query<'_, '_> {
Query {
reader: self,
exact_regex,
hash: None,
package_pattern: None,
}
}
#[allow(clippy::print_stdout)]
pub fn dump(&mut self) -> Result<()> {
loop {
let block = self.decoder.decode()?;
if block.is_empty() {
break;
}
for line in block.split(|c| *c == b'\n') {
println!("{:?}", String::from_utf8_lossy(line));
}
println!("-- block boundary");
}
Ok(())
}
}
pub struct Query<'a, 'b> {
reader: Reader,
exact_regex: &'a Regex,
hash: Option<String>,
package_pattern: Option<&'b Regex>,
}
impl<'a, 'b> Query<'a, 'b> {
pub fn hash(self, hash: Option<String>) -> Query<'a, 'b> {
Query { hash, ..self }
}
pub fn package_pattern(self, package_pattern: Option<&'b Regex>) -> Query<'a, 'b> {
Query {
package_pattern,
..self
}
}
pub fn run(self) -> Result<ReaderIter<'a, 'b>> {
let mut expr = regex_syntax::ast::parse::Parser::new()
.parse(self.exact_regex.as_str())
.expect("regex cannot be invalid");
{
let mut stack = vec![&mut expr];
while let Some(e) = stack.pop() {
match e {
Ast::Assertion(a) if a.kind == AssertionKind::StartLine => {
*e = Ast::Literal(Box::new(Literal {
span: a.span,
c: '\0',
kind: regex_syntax::ast::LiteralKind::Verbatim,
}))
}
Ast::Group(g) => stack.push(&mut g.ast),
Ast::Repetition(r) => stack.push(&mut r.ast),
Ast::Concat(c) => stack.extend(c.asts.iter_mut()),
Ast::Alternation(a) => stack.extend(a.asts.iter_mut()),
_ => {}
}
}
}
let mut regex_builder = grep::regex::RegexMatcherBuilder::new();
regex_builder.line_terminator(Some(b'\n')).multi_line(true);
let grep = regex_builder.build(&format!("{}", expr))?;
Ok(ReaderIter {
reader: self.reader,
found: Vec::new(),
found_without_package: Vec::new(),
pattern: grep,
exact_pattern: self.exact_regex,
package_entry_pattern: regex_builder.build("^p\0").expect("valid regex"),
package_name_pattern: self.package_pattern,
package_hash: self.hash,
})
}
}
pub struct ReaderIter<'a, 'b> {
reader: Reader,
found: Vec<(StorePath, FileTreeEntry)>,
found_without_package: Vec<FileTreeEntry>,
pattern: grep::regex::RegexMatcher,
exact_pattern: &'a Regex,
package_entry_pattern: grep::regex::RegexMatcher,
package_name_pattern: Option<&'b Regex>,
package_hash: Option<String>,
}
fn consume_no_error<T>(e: NoError) -> T {
panic!("impossible: {}", e)
}
fn next_matching_line<M: Matcher<Error = NoError>>(
matcher: M,
buf: &[u8],
mut start: usize,
) -> Option<Match> {
while let Some(candidate) = matcher
.find_candidate_line(&buf[start..])
.unwrap_or_else(consume_no_error)
{
if start == buf.len() {
return None;
};
let (pos, confirmed) = match candidate {
LineMatchKind::Confirmed(pos) => (start + pos, true),
LineMatchKind::Candidate(pos) => (start + pos, false),
};
let line_start = memrchr(b'\n', &buf[..pos]).map_or(0, |x| x + 1);
let line_end = memchr(b'\n', &buf[pos..]).map_or(buf.len(), |x| x + pos + 1);
if !confirmed
&& !matcher
.is_match(&buf[line_start..line_end])
.unwrap_or_else(consume_no_error)
{
start = line_end;
continue;
}
return Some(Match::new(line_start, line_end));
}
None
}
impl<'a, 'b> ReaderIter<'a, 'b> {
fn fill_buf(&mut self) -> Result<()> {
while self.found.is_empty() {
let &mut ReaderIter {
ref mut reader,
ref package_entry_pattern,
ref package_name_pattern,
ref package_hash,
..
} = self;
let block = reader.decoder.decode()?;
if block.is_empty() {
return Ok(());
}
let mut cached_package: Option<(StorePath, usize)> = None;
let mut no_more_package = false;
let mut find_package = |item_end| -> Result<_> {
if let Some((ref pkg, end)) = cached_package {
if item_end < end {
return Ok(Some((pkg.clone(), end)));
}
}
if no_more_package {
return Ok(None);
}
let mat = match next_matching_line(package_entry_pattern, block, item_end) {
Some(v) => v,
None => {
no_more_package = true;
return Ok(None);
}
};
let json = &block[mat.start() + 2..mat.end() - 1];
let pkg: StorePath =
serde_json::from_slice(json).map_err(|_| Error::StorePathParse {
path: json.to_vec(),
})?;
cached_package = Some((pkg.clone(), mat.end()));
Ok(Some((pkg, mat.end())))
};
let should_search_package = |pkg: &StorePath| -> bool {
package_name_pattern.is_none_or(|r| r.is_match(pkg.name().as_bytes()))
&& package_hash.as_ref().is_none_or(|h| h == &pkg.hash())
};
let mut pos = 0;
if !self.found_without_package.is_empty() {
if let Some((pkg, end)) = find_package(0)? {
if !should_search_package(&pkg) {
pos = end;
self.found_without_package.truncate(0);
} else {
for entry in self.found_without_package.split_off(0) {
self.found.push((pkg.clone(), entry));
}
}
}
}
while let Some(mat) = next_matching_line(&self.pattern, block, pos) {
pos = mat.end();
let entry = &block[mat.start()..mat.end() - 1];
if self
.package_entry_pattern
.is_match(entry)
.unwrap_or_else(consume_no_error)
{
continue;
}
if let Some((pkg, end)) = find_package(mat.end())? {
if !should_search_package(&pkg) {
pos = end;
continue;
}
}
let entry = FileTreeEntry::decode(entry).ok_or_else(|| Error::EntryParse {
entry: entry.to_vec(),
})?;
if !self.exact_pattern.is_match(&entry.path) {
continue;
}
match find_package(mat.end())? {
None => self.found_without_package.push(entry),
Some((pkg, _)) => self.found.push((pkg, entry)),
}
}
}
Ok(())
}
fn next_match(&mut self) -> Result<Option<(StorePath, FileTreeEntry)>> {
self.fill_buf()?;
Ok(self.found.pop())
}
}
impl<'a, 'b> Iterator for ReaderIter<'a, 'b> {
type Item = Result<(StorePath, FileTreeEntry)>;
fn next(&mut self) -> Option<Self::Item> {
match self.next_match() {
Err(e) => Some(Err(e)),
Ok(v) => v.map(Ok),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_next_matching_line_package() {
let matcher = grep::regex::RegexMatcherBuilder::new()
.line_terminator(Some(b'\n'))
.multi_line(true)
.build("^p")
.expect("valid regex");
let buffer = br#"
SOME LINE
pDATA
ANOTHER LINE
"#;
let mat = next_matching_line(matcher, buffer, 0);
assert_eq!(mat, Some(Match::new(11, 17)));
}
}