Skip to main content

nix_index/
database.rs

1use std::fs::File;
2/// Creating and searching file databases.
3///
4/// This module implements an abstraction for creating an index of files with meta information
5/// and searching that index for paths matching a specific pattern.
6use std::io::{self, BufReader, BufWriter, Read, Seek, Write};
7use std::path::Path;
8
9use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
10use grep;
11use grep::matcher::{LineMatchKind, Match, Matcher, NoError};
12use memchr::{memchr, memrchr};
13use regex::bytes::Regex;
14use regex_syntax::ast::{AssertionKind, Ast, Literal};
15use serde_json;
16use thiserror::Error;
17use zstd;
18
19use crate::files::{FileTree, FileTreeEntry};
20use crate::frcode;
21use crate::package::StorePath;
22
23/// The version of the database format supported by this nix-index version.
24///
25/// This should be updated whenever you make an incompatible change to the database format.
26const FORMAT_VERSION: u64 = 1;
27
28/// The magic for nix-index database files, used to ensure that the file we're passed is
29/// actually a file generated by nix-index.
30const FILE_MAGIC: &[u8] = b"NIXI";
31
32/// A writer for creating a new file database.
33pub struct Writer {
34    /// The encoder used to compress the database. Will be set to `None` when the value
35    /// is dropped.
36    writer: Option<BufWriter<zstd::Encoder<'static, File>>>,
37}
38
39// We need to make sure that the encoder is `finish`ed in all cases, so we need
40// a custom Drop.
41impl Drop for Writer {
42    fn drop(&mut self) {
43        if self.writer.is_some() {
44            self.finish_encoder().expect("failed to flush database");
45        }
46    }
47}
48
49impl Writer {
50    /// Creates a new database at the given path with the specified zstd compression level
51    /// (currently, supported values range from 0 to 22).
52    pub fn create<P: AsRef<Path>>(path: P, level: i32) -> io::Result<Writer> {
53        let mut file = File::create(path)?;
54        file.write_all(FILE_MAGIC)?;
55        file.write_u64::<LittleEndian>(FORMAT_VERSION)?;
56        let mut encoder = zstd::Encoder::new(file, level)?;
57        encoder.multithread(num_cpus::get() as u32)?;
58
59        Ok(Writer {
60            writer: Some(BufWriter::new(encoder)),
61        })
62    }
63
64    /// Add a new package to the database for the given store path with its corresponding
65    /// file tree. Entries are only added if they match `filter_prefix`.
66    pub fn add(
67        &mut self,
68        path: StorePath,
69        files: FileTree,
70        filter_prefix: &[u8],
71    ) -> io::Result<()> {
72        let entries = files.to_list(filter_prefix);
73
74        // Don't add packages with no file entries to the database.
75        if entries.is_empty() {
76            return Ok(());
77        }
78        let writer = self.writer.as_mut().expect("not dropped yet");
79        let mut encoder = frcode::Encoder::new(
80            writer,
81            b"p".to_vec(),
82            serde_json::to_vec(&path).expect("failed to serialize path"),
83        );
84        for entry in entries {
85            entry.encode(&mut encoder)?;
86        }
87        Ok(())
88    }
89
90    /// Finishes encoding. After calling this function, `add` may no longer be called, since this function
91    /// closes the stream.
92    ///
93    /// The return value is the underlying File.
94    fn finish_encoder(&mut self) -> io::Result<File> {
95        let writer = self.writer.take().expect("not dropped yet");
96        let encoder = writer.into_inner()?;
97        encoder.finish()
98    }
99
100    /// Finish the encoding and return the size in bytes of the compressed file that was created.
101    pub fn finish(mut self) -> io::Result<u64> {
102        let mut file = self.finish_encoder()?;
103        file.stream_position()
104    }
105}
106
107#[derive(Error, Debug)]
108pub enum Error {
109    #[error("expected file to start with nix-index file magic 'NIXI', but found '{found:?}' (is this a valid nix-index database file?)")]
110    UnsupportedFileType { found: Vec<u8> },
111    #[error("this executable only supports the nix-index database version {}, but found a database with version {found}", FORMAT_VERSION)]
112    UnsupportedVersion { found: u64 },
113    #[error("database corrupt, found a file entry without a matching package entry")]
114    MissingPackageEntry,
115    #[error("database corrupt, frcode error: {0}")]
116    Frcode(#[from] frcode::Error),
117    #[error("database corrupt, could not parse entry: {entry:?}")]
118    EntryParse { entry: Vec<u8> },
119    #[error("database corrupt, could not parse store path: {path:?}")]
120    StorePathParse { path: Vec<u8> },
121    #[error("I/O error: {0}")]
122    Io(#[from] io::Error),
123    #[error("grep error: {0}")]
124    Grep(#[from] grep::regex::Error),
125}
126
127type Result<T> = std::result::Result<T, Error>;
128
129/// A Reader allows fast querying of a nix-index database.
130pub struct Reader {
131    decoder: frcode::Decoder<BufReader<zstd::Decoder<'static, BufReader<File>>>>,
132}
133
134impl Reader {
135    /// Opens a nix-index database located at the given path.
136    ///
137    /// If the path does not exist or is not a valid database, an error is returned.
138    pub fn open<P: AsRef<Path>>(path: P) -> Result<Reader> {
139        let mut file = File::open(path)?;
140        let mut magic = [0u8; 4];
141        file.read_exact(&mut magic)?;
142
143        if magic != FILE_MAGIC {
144            return Err(Error::UnsupportedFileType {
145                found: magic.to_vec(),
146            });
147        }
148
149        let version = file.read_u64::<LittleEndian>()?;
150        if version != FORMAT_VERSION {
151            return Err(Error::UnsupportedVersion { found: version });
152        }
153
154        let decoder = zstd::Decoder::new(file)?;
155        Ok(Reader {
156            decoder: frcode::Decoder::new(BufReader::new(decoder)),
157        })
158    }
159
160    /// Builds a query to find all entries in the database that have a filename matching the given pattern.
161    ///
162    /// Afterwards, use `Query::into_iter` to iterate over the items.
163    pub fn query(self, exact_regex: &Regex) -> Query<'_, '_> {
164        Query {
165            reader: self,
166            exact_regex,
167            hash: None,
168            package_pattern: None,
169        }
170    }
171
172    /// Dumps the contents of the database to stdout, for debugging.
173    #[allow(clippy::print_stdout)]
174    pub fn dump(&mut self) -> Result<()> {
175        loop {
176            let block = self.decoder.decode()?;
177            if block.is_empty() {
178                break;
179            }
180            for line in block.split(|c| *c == b'\n') {
181                println!("{:?}", String::from_utf8_lossy(line));
182            }
183            println!("-- block boundary");
184        }
185        Ok(())
186    }
187}
188
189/// A builder for a `ReaderIter` to iterate over entries in the database matching a given pattern.
190pub struct Query<'a, 'b> {
191    /// The underlying reader from which we read input.
192    reader: Reader,
193
194    /// The pattern that file paths have to match.
195    exact_regex: &'a Regex,
196
197    /// Only include the package with the given hash.
198    hash: Option<String>,
199
200    /// Only include packages whose name matches the given pattern.
201    package_pattern: Option<&'b Regex>,
202}
203
204impl<'a, 'b> Query<'a, 'b> {
205    /// Limit results to entries from the package with the specified hash if `Some`.
206    pub fn hash(self, hash: Option<String>) -> Query<'a, 'b> {
207        Query { hash, ..self }
208    }
209
210    /// Limit results to entries from packages whose name matches the given regex if `Some`.
211    pub fn package_pattern(self, package_pattern: Option<&'b Regex>) -> Query<'a, 'b> {
212        Query {
213            package_pattern,
214            ..self
215        }
216    }
217
218    /// Runs the query, returning an Iterator that will yield all entries matching the conditions.
219    ///
220    /// There is no guarantee about the order of the returned matches.
221    pub fn run(self) -> Result<ReaderIter<'a, 'b>> {
222        let mut expr = regex_syntax::ast::parse::Parser::new()
223            .parse(self.exact_regex.as_str())
224            .expect("regex cannot be invalid");
225        // replace the ^ anchor by a NUL byte, since each entry is of the form `METADATA\0PATH`
226        // (so the NUL byte marks the start of the path).
227        {
228            let mut stack = vec![&mut expr];
229            while let Some(e) = stack.pop() {
230                match e {
231                    Ast::Assertion(a) if a.kind == AssertionKind::StartLine => {
232                        *e = Ast::Literal(Box::new(Literal {
233                            span: a.span,
234                            c: '\0',
235                            kind: regex_syntax::ast::LiteralKind::Verbatim,
236                        }))
237                    }
238                    Ast::Group(g) => stack.push(&mut g.ast),
239                    Ast::Repetition(r) => stack.push(&mut r.ast),
240                    Ast::Concat(c) => stack.extend(c.asts.iter_mut()),
241                    Ast::Alternation(a) => stack.extend(a.asts.iter_mut()),
242                    _ => {}
243                }
244            }
245        }
246        let mut regex_builder = grep::regex::RegexMatcherBuilder::new();
247        regex_builder.line_terminator(Some(b'\n')).multi_line(true);
248
249        let grep = regex_builder.build(&format!("{}", expr))?;
250        Ok(ReaderIter {
251            reader: self.reader,
252            found: Vec::new(),
253            found_without_package: Vec::new(),
254            pattern: grep,
255            exact_pattern: self.exact_regex,
256            package_entry_pattern: regex_builder.build("^p\0").expect("valid regex"),
257            package_name_pattern: self.package_pattern,
258            package_hash: self.hash,
259        })
260    }
261}
262
263/// An iterator for entries in a database matching a given pattern.
264pub struct ReaderIter<'a, 'b> {
265    /// The underlying reader from which we read input.
266    reader: Reader,
267    /// Entries that matched the pattern but have not been returned by `next` yet.
268    found: Vec<(StorePath, FileTreeEntry)>,
269    /// Entries that matched the pattern but for which we don't know yet what package they belong to.
270    /// This may happen if the entry we matched was at the end of the search buffer, so that the entry
271    /// for the package did not fit into the buffer anymore (since the package is stored after the entries
272    /// of the package). In this case, we need to look for the package entry in the next iteration when
273    /// we read the next block of input.
274    found_without_package: Vec<FileTreeEntry>,
275    /// The pattern for which to search package paths.
276    ///
277    /// This pattern should work on the raw bytes of file entries. In particular, the file path is not the
278    /// first data in a file entry, so the regex `^` anchor will not work correctly.
279    ///
280    /// The pattern here may produce false positives (for example, if it matches inside the metadata of a file
281    /// entry). This is not a problem, as matches are later checked against `exact_pattern`.
282    pattern: grep::regex::RegexMatcher,
283    /// The raw pattern, as supplied to `find_iter`. This is used to verify matches, since `pattern` itself
284    /// may produce false positives.
285    exact_pattern: &'a Regex,
286    /// Pattern that matches only package entries.
287    package_entry_pattern: grep::regex::RegexMatcher,
288    /// Pattern that the package name should match.
289    package_name_pattern: Option<&'b Regex>,
290    /// Only search the package with the given hash.
291    package_hash: Option<String>,
292}
293
294fn consume_no_error<T>(e: NoError) -> T {
295    panic!("impossible: {}", e)
296}
297
298fn next_matching_line<M: Matcher<Error = NoError>>(
299    matcher: M,
300    buf: &[u8],
301    mut start: usize,
302) -> Option<Match> {
303    while let Some(candidate) = matcher
304        .find_candidate_line(&buf[start..])
305        .unwrap_or_else(consume_no_error)
306    {
307        // the buffer may end with a newline character, so we may get a match
308        // for an empty "line" at the end of the buffer
309        // since this is not a line match, return None
310        if start == buf.len() {
311            return None;
312        };
313
314        let (pos, confirmed) = match candidate {
315            LineMatchKind::Confirmed(pos) => (start + pos, true),
316            LineMatchKind::Candidate(pos) => (start + pos, false),
317        };
318
319        let line_start = memrchr(b'\n', &buf[..pos]).map_or(0, |x| x + 1);
320        let line_end = memchr(b'\n', &buf[pos..]).map_or(buf.len(), |x| x + pos + 1);
321
322        if !confirmed
323            && !matcher
324                .is_match(&buf[line_start..line_end])
325                .unwrap_or_else(consume_no_error)
326        {
327            start = line_end;
328            continue;
329        }
330
331        return Some(Match::new(line_start, line_end));
332    }
333    None
334}
335
336impl<'a, 'b> ReaderIter<'a, 'b> {
337    /// Reads input until `self.found` contains at least one entry or the end of the input has been reached.
338    fn fill_buf(&mut self) -> Result<()> {
339        // the input is processed in blocks until we've found at least a single entry
340        while self.found.is_empty() {
341            let &mut ReaderIter {
342                ref mut reader,
343                ref package_entry_pattern,
344                ref package_name_pattern,
345                ref package_hash,
346                ..
347            } = self;
348            let block = reader.decoder.decode()?;
349
350            // if the block is empty, the end of input has been reached
351            if block.is_empty() {
352                return Ok(());
353            }
354
355            // when we find a match, we need to know the package that this match belongs to.
356            // the `find_package` function will skip forward until a package entry is found
357            // (the package entry comes after all file entries for a package).
358            //
359            // to be more efficient if there are many matches, we cache the current package here.
360            // this package is valid for all positions up to the second element of the tuple
361            // (after that, a new package begins).
362            let mut cached_package: Option<(StorePath, usize)> = None;
363            let mut no_more_package = false;
364            let mut find_package = |item_end| -> Result<_> {
365                if let Some((ref pkg, end)) = cached_package {
366                    if item_end < end {
367                        return Ok(Some((pkg.clone(), end)));
368                    }
369                }
370
371                if no_more_package {
372                    return Ok(None);
373                }
374
375                let mat = match next_matching_line(package_entry_pattern, block, item_end) {
376                    Some(v) => v,
377                    None => {
378                        no_more_package = true;
379                        return Ok(None);
380                    }
381                };
382
383                let json = &block[mat.start() + 2..mat.end() - 1];
384                let pkg: StorePath =
385                    serde_json::from_slice(json).map_err(|_| Error::StorePathParse {
386                        path: json.to_vec(),
387                    })?;
388                cached_package = Some((pkg.clone(), mat.end()));
389                Ok(Some((pkg, mat.end())))
390            };
391
392            // Tests if a store path matches the `package_name_pattern` and `package_hash` constraints.
393            let should_search_package = |pkg: &StorePath| -> bool {
394                package_name_pattern.is_none_or(|r| r.is_match(pkg.name().as_bytes()))
395                    && package_hash.as_ref().is_none_or(|h| h == &pkg.hash())
396            };
397
398            let mut pos = 0;
399            // if there are any entries without a package left over from the previous iteration, see
400            // if this block contains the package entry.
401            if !self.found_without_package.is_empty() {
402                if let Some((pkg, end)) = find_package(0)? {
403                    if !should_search_package(&pkg) {
404                        // all entries before end will have the same package
405                        pos = end;
406                        self.found_without_package.truncate(0);
407                    } else {
408                        for entry in self.found_without_package.split_off(0) {
409                            self.found.push((pkg.clone(), entry));
410                        }
411                    }
412                }
413            }
414
415            // process all matches in this block
416            while let Some(mat) = next_matching_line(&self.pattern, block, pos) {
417                pos = mat.end();
418                let entry = &block[mat.start()..mat.end() - 1];
419                // skip entries that aren't describing file paths
420                if self
421                    .package_entry_pattern
422                    .is_match(entry)
423                    .unwrap_or_else(consume_no_error)
424                {
425                    continue;
426                }
427
428                // skip if package name or hash doesn't match
429                // we can only skip if we know the package
430                if let Some((pkg, end)) = find_package(mat.end())? {
431                    if !should_search_package(&pkg) {
432                        // all entries before end will have the same package
433                        pos = end;
434                        continue;
435                    }
436                }
437
438                let entry = FileTreeEntry::decode(entry).ok_or_else(|| Error::EntryParse {
439                    entry: entry.to_vec(),
440                })?;
441
442                // check for false positives
443                if !self.exact_pattern.is_match(&entry.path) {
444                    continue;
445                }
446
447                match find_package(mat.end())? {
448                    None => self.found_without_package.push(entry),
449                    Some((pkg, _)) => self.found.push((pkg, entry)),
450                }
451            }
452        }
453        Ok(())
454    }
455
456    /// Returns the next match in the database.
457    fn next_match(&mut self) -> Result<Option<(StorePath, FileTreeEntry)>> {
458        self.fill_buf()?;
459        Ok(self.found.pop())
460    }
461}
462
463impl<'a, 'b> Iterator for ReaderIter<'a, 'b> {
464    type Item = Result<(StorePath, FileTreeEntry)>;
465
466    fn next(&mut self) -> Option<Self::Item> {
467        match self.next_match() {
468            Err(e) => Some(Err(e)),
469            Ok(v) => v.map(Ok),
470        }
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn test_next_matching_line_package() {
480        let matcher = grep::regex::RegexMatcherBuilder::new()
481            .line_terminator(Some(b'\n'))
482            .multi_line(true)
483            .build("^p")
484            .expect("valid regex");
485        let buffer = br#"
486SOME LINE
487pDATA
488ANOTHER LINE
489        "#;
490
491        let mat = next_matching_line(matcher, buffer, 0);
492        assert_eq!(mat, Some(Match::new(11, 17)));
493    }
494}