1use serde::{Deserialize, Serialize};
9use tatara_lisp::DeriveTataraDomain;
10
11use crate::nar;
12use crate::store_inventory::{RefIndex, StoreEntry};
13use crate::SpecError;
14
15#[derive(Debug, Clone)]
18pub enum StorePredicate {
19 NameMatches(String),
21 SizeAtLeast(u64),
23 SizeAtMost(u64),
25 FileCountAtLeast(usize),
27 ContainsBytes(Vec<u8>),
30 ContentsMatch(String),
32 HasReference(String),
35 All(Vec<StorePredicate>),
37 Any(Vec<StorePredicate>),
39 Not(Box<StorePredicate>),
41}
42
43pub fn matches(
47 entry: &StoreEntry,
48 predicate: &StorePredicate,
49 idx: Option<&RefIndex>,
50) -> bool {
51 match predicate {
52 StorePredicate::NameMatches(pattern) => {
53 regex::Regex::new(pattern).is_ok_and(|re| re.is_match(&entry.parsed.name))
54 }
55 StorePredicate::SizeAtLeast(n) => entry.size >= *n,
56 StorePredicate::SizeAtMost(n) => entry.size <= *n,
57 StorePredicate::FileCountAtLeast(n) => entry.file_count >= *n,
58 StorePredicate::ContainsBytes(needle) => {
59 let nar = match nar::encode(&entry.path) {
60 Ok(b) => b,
61 Err(_) => return false,
62 };
63 nar.windows(needle.len()).any(|w| w == needle)
64 }
65 StorePredicate::ContentsMatch(pattern) => {
66 let re = match regex::bytes::Regex::new(pattern) {
67 Ok(r) => r,
68 Err(_) => return false,
69 };
70 let nar = match nar::encode(&entry.path) {
71 Ok(b) => b,
72 Err(_) => return false,
73 };
74 re.is_match(&nar)
75 }
76 StorePredicate::HasReference(substr) => {
77 if let Some(idx) = idx {
78 idx.refs_from(&entry.path).iter().any(|p| {
79 p.to_string_lossy().contains(substr)
80 })
81 } else {
82 false
83 }
84 }
85 StorePredicate::All(ps) => ps.iter().all(|p| matches(entry, p, idx)),
86 StorePredicate::Any(ps) => ps.iter().any(|p| matches(entry, p, idx)),
87 StorePredicate::Not(p) => !matches(entry, p, idx),
88 }
89}
90
91#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
102#[tatara(keyword = "defstore-query")]
103pub struct StoreQuery {
104 pub name: String,
106 pub description: String,
108 #[serde(rename = "nameRegex", default)]
110 pub name_regex: String,
111 #[serde(rename = "minSize", default)]
113 pub min_size: u64,
114 #[serde(rename = "maxSize", default)]
116 pub max_size: u64,
117 #[serde(rename = "contentsRegex", default)]
119 pub contents_regex: String,
120 #[serde(rename = "hasReference", default)]
122 pub has_reference: String,
123}
124
125impl StoreQuery {
126 #[must_use]
129 pub fn to_predicate(&self) -> StorePredicate {
130 let mut clauses = Vec::new();
131 if !self.name_regex.is_empty() {
132 clauses.push(StorePredicate::NameMatches(self.name_regex.clone()));
133 }
134 if self.min_size > 0 {
135 clauses.push(StorePredicate::SizeAtLeast(self.min_size));
136 }
137 if self.max_size > 0 {
138 clauses.push(StorePredicate::SizeAtMost(self.max_size));
139 }
140 if !self.contents_regex.is_empty() {
141 clauses.push(StorePredicate::ContentsMatch(self.contents_regex.clone()));
142 }
143 if !self.has_reference.is_empty() {
144 clauses.push(StorePredicate::HasReference(self.has_reference.clone()));
145 }
146 if clauses.is_empty() {
147 StorePredicate::All(vec![])
150 } else {
151 StorePredicate::All(clauses)
152 }
153 }
154}
155
156pub const CANONICAL_STORE_QUERIES_LISP: &str =
157 include_str!("../specs/store_queries.lisp");
158
159pub fn load_canonical() -> Result<Vec<StoreQuery>, SpecError> {
165 crate::loader::load_all::<StoreQuery>(CANONICAL_STORE_QUERIES_LISP)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::store_inventory::StoreEntry;
172 use crate::store_layout::ParsedStorePath;
173
174 fn entry(name: &str, size: u64, file_count: usize) -> StoreEntry {
175 StoreEntry {
176 path: std::path::PathBuf::from(format!("/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-{name}")),
177 parsed: ParsedStorePath {
178 algorithm: None,
179 hash: "a".repeat(32),
180 name: name.to_string(),
181 sub_path: None,
182 },
183 is_directory: false,
184 file_count,
185 size,
186 }
187 }
188
189 #[test]
190 fn name_matches_regex() {
191 let e = entry("hello-2.12", 0, 1);
192 assert!(matches(&e, &StorePredicate::NameMatches("^hello-.*".into()), None));
193 assert!(!matches(&e, &StorePredicate::NameMatches("^bye".into()), None));
194 }
195
196 #[test]
197 fn size_predicates_work() {
198 let e = entry("x", 100, 1);
199 assert!(matches(&e, &StorePredicate::SizeAtLeast(50), None));
200 assert!(!matches(&e, &StorePredicate::SizeAtLeast(150), None));
201 assert!(matches(&e, &StorePredicate::SizeAtMost(100), None));
202 }
203
204 #[test]
205 fn and_or_not_compose() {
206 let e = entry("hello-1.0", 100, 1);
207 let p = StorePredicate::All(vec![
208 StorePredicate::NameMatches("^hello".into()),
209 StorePredicate::SizeAtLeast(50),
210 StorePredicate::Not(Box::new(StorePredicate::SizeAtLeast(500))),
211 ]);
212 assert!(matches(&e, &p, None));
213 }
214
215 #[test]
216 fn any_short_circuits_on_true() {
217 let e = entry("hello", 100, 1);
218 let p = StorePredicate::Any(vec![
219 StorePredicate::NameMatches("^hello".into()),
220 StorePredicate::SizeAtLeast(99999), ]);
222 assert!(matches(&e, &p, None));
223 }
224}