use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::nar;
use crate::store_inventory::{RefIndex, StoreEntry};
use crate::SpecError;
#[derive(Debug, Clone)]
pub enum StorePredicate {
NameMatches(String),
SizeAtLeast(u64),
SizeAtMost(u64),
FileCountAtLeast(usize),
ContainsBytes(Vec<u8>),
ContentsMatch(String),
HasReference(String),
All(Vec<StorePredicate>),
Any(Vec<StorePredicate>),
Not(Box<StorePredicate>),
}
pub fn matches(
entry: &StoreEntry,
predicate: &StorePredicate,
idx: Option<&RefIndex>,
) -> bool {
match predicate {
StorePredicate::NameMatches(pattern) => {
regex::Regex::new(pattern).is_ok_and(|re| re.is_match(&entry.parsed.name))
}
StorePredicate::SizeAtLeast(n) => entry.size >= *n,
StorePredicate::SizeAtMost(n) => entry.size <= *n,
StorePredicate::FileCountAtLeast(n) => entry.file_count >= *n,
StorePredicate::ContainsBytes(needle) => {
let nar = match nar::encode(&entry.path) {
Ok(b) => b,
Err(_) => return false,
};
nar.windows(needle.len()).any(|w| w == needle)
}
StorePredicate::ContentsMatch(pattern) => {
let re = match regex::bytes::Regex::new(pattern) {
Ok(r) => r,
Err(_) => return false,
};
let nar = match nar::encode(&entry.path) {
Ok(b) => b,
Err(_) => return false,
};
re.is_match(&nar)
}
StorePredicate::HasReference(substr) => {
if let Some(idx) = idx {
idx.refs_from(&entry.path).iter().any(|p| {
p.to_string_lossy().contains(substr)
})
} else {
false
}
}
StorePredicate::All(ps) => ps.iter().all(|p| matches(entry, p, idx)),
StorePredicate::Any(ps) => ps.iter().any(|p| matches(entry, p, idx)),
StorePredicate::Not(p) => !matches(entry, p, idx),
}
}
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defstore-query")]
pub struct StoreQuery {
pub name: String,
pub description: String,
#[serde(rename = "nameRegex", default)]
pub name_regex: String,
#[serde(rename = "minSize", default)]
pub min_size: u64,
#[serde(rename = "maxSize", default)]
pub max_size: u64,
#[serde(rename = "contentsRegex", default)]
pub contents_regex: String,
#[serde(rename = "hasReference", default)]
pub has_reference: String,
}
impl StoreQuery {
#[must_use]
pub fn to_predicate(&self) -> StorePredicate {
let mut clauses = Vec::new();
if !self.name_regex.is_empty() {
clauses.push(StorePredicate::NameMatches(self.name_regex.clone()));
}
if self.min_size > 0 {
clauses.push(StorePredicate::SizeAtLeast(self.min_size));
}
if self.max_size > 0 {
clauses.push(StorePredicate::SizeAtMost(self.max_size));
}
if !self.contents_regex.is_empty() {
clauses.push(StorePredicate::ContentsMatch(self.contents_regex.clone()));
}
if !self.has_reference.is_empty() {
clauses.push(StorePredicate::HasReference(self.has_reference.clone()));
}
if clauses.is_empty() {
StorePredicate::All(vec![])
} else {
StorePredicate::All(clauses)
}
}
}
pub const CANONICAL_STORE_QUERIES_LISP: &str =
include_str!("../specs/store_queries.lisp");
pub fn load_canonical() -> Result<Vec<StoreQuery>, SpecError> {
crate::loader::load_all::<StoreQuery>(CANONICAL_STORE_QUERIES_LISP)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store_inventory::StoreEntry;
use crate::store_layout::ParsedStorePath;
fn entry(name: &str, size: u64, file_count: usize) -> StoreEntry {
StoreEntry {
path: std::path::PathBuf::from(format!("/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-{name}")),
parsed: ParsedStorePath {
algorithm: None,
hash: "a".repeat(32),
name: name.to_string(),
sub_path: None,
},
is_directory: false,
file_count,
size,
}
}
#[test]
fn name_matches_regex() {
let e = entry("hello-2.12", 0, 1);
assert!(matches(&e, &StorePredicate::NameMatches("^hello-.*".into()), None));
assert!(!matches(&e, &StorePredicate::NameMatches("^bye".into()), None));
}
#[test]
fn size_predicates_work() {
let e = entry("x", 100, 1);
assert!(matches(&e, &StorePredicate::SizeAtLeast(50), None));
assert!(!matches(&e, &StorePredicate::SizeAtLeast(150), None));
assert!(matches(&e, &StorePredicate::SizeAtMost(100), None));
}
#[test]
fn and_or_not_compose() {
let e = entry("hello-1.0", 100, 1);
let p = StorePredicate::All(vec![
StorePredicate::NameMatches("^hello".into()),
StorePredicate::SizeAtLeast(50),
StorePredicate::Not(Box::new(StorePredicate::SizeAtLeast(500))),
]);
assert!(matches(&e, &p, None));
}
#[test]
fn any_short_circuits_on_true() {
let e = entry("hello", 100, 1);
let p = StorePredicate::Any(vec![
StorePredicate::NameMatches("^hello".into()),
StorePredicate::SizeAtLeast(99999), ]);
assert!(matches(&e, &p, None));
}
}