Skip to main content

spam_db/
options.rs

1use std::path::Path;
2
3use crate::{
4  Error, Result,
5  format::{DbFile, DbKind},
6};
7
8/// A single NixOS module option from an options database.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct OptionRecord {
11  /// Fully-qualified option name, e.g. `"services.nginx.enable"`.
12  pub name: String,
13  /// Summary text (description, type, or default), if present in the database.
14  pub summary: Option<String>,
15}
16
17/// Handle to an open spam options database.
18///
19/// Only the fixed-size bucket index is loaded into memory on construction.
20#[derive(Debug)]
21pub struct OptionsDb {
22  db: DbFile,
23}
24
25impl OptionsDb {
26  pub(crate) fn from_file(db: DbFile) -> Self {
27    Self { db }
28  }
29
30  /// Open the options database at `path`.
31  ///
32  /// Returns [`Error::InvalidDatabase`] if the file is not an options database.
33  pub fn open(path: impl AsRef<Path>) -> Result<Self> {
34    let db = DbFile::open(path)?;
35    if db.kind != DbKind::Options {
36      return Err(Error::InvalidDatabase(
37        "expected an options database (kind = options)".into(),
38      ));
39    }
40    Ok(Self { db })
41  }
42
43  /// Return all records whose name contains `query` as a substring.
44  ///
45  /// Decompresses only the bucket for `query[0]`. An empty query reads bucket 0.
46  pub fn query(&self, query: &str) -> Result<Vec<OptionRecord>> {
47    let bucket = DbFile::query_bucket(query);
48    let lines = self.db.bucket_lines(bucket)?;
49
50    let mut records = Vec::new();
51    for line in &lines {
52      let (name, summary) = split_tab(line);
53      if name.contains(query) {
54        records.push(OptionRecord {
55          name: name.to_owned(),
56          summary: summary.filter(|s| !s.is_empty()).map(str::to_owned),
57        });
58      }
59    }
60    Ok(records)
61  }
62}
63
64/// Split a database line on the first tab, returning `(key, Some(value))` or
65/// `(whole_line, None)` when no tab is present.
66fn split_tab(line: &str) -> (&str, Option<&str>) {
67  match line.find('\t') {
68    Some(tab) => (&line[..tab], Some(&line[tab + 1..])),
69    None => (line, None),
70  }
71}