Skip to main content

spam_db/
packages.rs

1use std::path::Path;
2
3use crate::{
4  Error, Result,
5  format::{DbFile, DbKind},
6};
7
8/// The type of a file entry in the store.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FileKind {
11  /// A regular file.
12  Regular,
13  /// A directory.
14  Directory,
15  /// A symbolic link.
16  Symlink,
17}
18
19impl Default for FileKind {
20  fn default() -> Self {
21    FileKind::Regular
22  }
23}
24
25/// A file-to-package mapping from a packages database.
26///
27/// Extended databases (produced by `spam index`) include full metadata;
28/// legacy databases (from `spam db build`) only populate `path` and `packages`.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct FileRecord {
31  /// Relative path within a Nix store output, e.g. `"/bin/hello"`.
32  pub path: String,
33  /// Package names that ship this file.
34  pub packages: Vec<String>,
35  /// File size in bytes (0 for directories and symlinks, and for legacy records).
36  pub size: u64,
37  /// File type.
38  pub kind: FileKind,
39  /// Whether the file has the executable bit set (meaningful for [`FileKind::Regular`] only).
40  pub executable: bool,
41  /// Symlink target; empty unless `kind == FileKind::Symlink`.
42  pub target: String,
43}
44
45/// Handle to an open spam packages database.
46///
47/// Only the fixed-size bucket index is loaded into memory on construction.
48#[derive(Debug)]
49pub struct PackagesDb {
50  db: DbFile,
51}
52
53impl PackagesDb {
54  pub(crate) fn from_file(db: DbFile) -> Self {
55    Self { db }
56  }
57
58  /// Open the packages database at `path`.
59  ///
60  /// Returns [`Error::InvalidDatabase`] if the file is not a packages database.
61  pub fn open(path: impl AsRef<Path>) -> Result<Self> {
62    let db = DbFile::open(path)?;
63    if db.kind != DbKind::Packages {
64      return Err(Error::InvalidDatabase(
65        "expected a packages database (kind = packages)".into(),
66      ));
67    }
68    Ok(Self { db })
69  }
70
71  /// Return all records whose path contains `query` as a substring.
72  ///
73  /// Decompresses only the bucket for `query[0]`. An empty query reads bucket 0.
74  ///
75  /// Both the legacy format (`path\tpkg1,pkg2,...`) and the extended format
76  /// (`path\tkind\tsize\texec\ttarget\tpkg1,pkg2,...`) are supported.
77  pub fn query(&self, query: &str) -> Result<Vec<FileRecord>> {
78    let bucket = DbFile::query_bucket(query);
79    let lines = self.db.bucket_lines(bucket)?;
80
81    let mut records = Vec::new();
82    for line in &lines {
83      let parts: Vec<&str> = line.splitn(7, '\t').collect();
84      if parts.is_empty() {
85        continue;
86      }
87      let path = parts[0];
88      if !path.contains(query) {
89        continue;
90      }
91
92      let record = if parts.len() >= 6 {
93        // Extended format: path\tkind\tsize\texec\ttarget\tpkg1,pkg2,...
94        let kind = match parts[1] {
95          "d" => FileKind::Directory,
96          "s" => FileKind::Symlink,
97          _ => FileKind::Regular,
98        };
99        let size: u64 = parts[2].parse().unwrap_or(0);
100        let executable = parts[3] == "1";
101        let target = parts[4].to_owned();
102        let packages = parts[5]
103          .split(',')
104          .filter(|s| !s.is_empty())
105          .map(str::to_owned)
106          .collect();
107        FileRecord {
108          path: path.to_owned(),
109          packages,
110          size,
111          kind,
112          executable,
113          target,
114        }
115      } else if parts.len() >= 2 {
116        // Legacy format: path\tpkg1,pkg2,...
117        let packages = parts[1]
118          .split(',')
119          .filter(|s| !s.is_empty())
120          .map(str::to_owned)
121          .collect();
122        FileRecord {
123          path: path.to_owned(),
124          packages,
125          size: 0,
126          kind: FileKind::Regular,
127          executable: false,
128          target: String::new(),
129        }
130      } else {
131        continue;
132      };
133
134      records.push(record);
135    }
136    Ok(records)
137  }
138}