1use std::path::Path;
2
3use crate::{
4 Error, Result,
5 format::{DbFile, DbKind},
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FileKind {
11 Regular,
13 Directory,
15 Symlink,
17}
18
19impl Default for FileKind {
20 fn default() -> Self {
21 FileKind::Regular
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct FileRecord {
31 pub path: String,
33 pub packages: Vec<String>,
35 pub size: u64,
37 pub kind: FileKind,
39 pub executable: bool,
41 pub target: String,
43}
44
45#[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 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
62 let db = DbFile::open(path)?;
63 if db.kind != DbKind::Packages && db.kind != DbKind::Index {
64 return Err(Error::InvalidDatabase(
65 "expected a packages or index database".into(),
66 ));
67 }
68 Ok(Self { db })
69 }
70
71 pub fn query(&self, query: &str) -> Result<Vec<FileRecord>> {
73 match self.db.kind {
74 DbKind::Packages => self.query_bucketed(query),
75 DbKind::Index => self.query_stream(query),
76 DbKind::Options => Err(Error::InvalidDatabase(
77 "expected a packages or index database".into(),
78 )),
79 }
80 }
81
82 fn query_bucketed(&self, query: &str) -> Result<Vec<FileRecord>> {
83 let bucket = DbFile::query_bucket(query);
84 let lines = self.db.bucket_lines(bucket)?;
85
86 let mut records = Vec::new();
87 for line in &lines {
88 let parts: Vec<&str> = line.splitn(7, '\t').collect();
89 if parts.is_empty() {
90 continue;
91 }
92 let path = parts[0];
93 if !path.contains(query) {
94 continue;
95 }
96
97 let record = if parts.len() >= 6 {
98 let kind = match parts[1] {
100 "d" => FileKind::Directory,
101 "s" => FileKind::Symlink,
102 _ => FileKind::Regular,
103 };
104 let size: u64 = parts[2].parse().unwrap_or(0);
105 let executable = parts[3] == "1";
106 let target = parts[4].to_owned();
107 let packages = parts[5]
108 .split(',')
109 .filter(|s| !s.is_empty())
110 .map(str::to_owned)
111 .collect();
112 FileRecord {
113 path: path.to_owned(),
114 packages,
115 size,
116 kind,
117 executable,
118 target,
119 }
120 } else if parts.len() >= 2 {
121 let packages = parts[1]
123 .split(',')
124 .filter(|s| !s.is_empty())
125 .map(str::to_owned)
126 .collect();
127 FileRecord {
128 path: path.to_owned(),
129 packages,
130 size: 0,
131 kind: FileKind::Regular,
132 executable: false,
133 target: String::new(),
134 }
135 } else {
136 continue;
137 };
138
139 records.push(record);
140 }
141 Ok(records)
142 }
143
144 fn query_stream(&self, query: &str) -> Result<Vec<FileRecord>> {
145 let lines = self.db.stream_lines()?;
146
147 let mut records = Vec::new();
148 let mut package = String::new();
149 let mut previous_path = String::new();
150 for line in &lines {
151 if let Some(package_name) = line.strip_prefix("P\t") {
152 package = package_name.to_owned();
153 previous_path.clear();
154 continue;
155 }
156
157 let Some(record) = parse_stream_record(line, &package, &mut previous_path) else {
158 continue;
159 };
160 if record.path.contains(query) {
161 records.push(record);
162 }
163 }
164 Ok(records)
165 }
166}
167
168fn parse_stream_record(
169 line: &str,
170 package: &str,
171 previous_path: &mut String,
172) -> Option<FileRecord> {
173 let parts: Vec<&str> = line.splitn(7, '\t').collect();
174 if parts.len() < 7 || parts[0] != "F" {
175 return None;
176 }
177
178 let shared = parts[5].parse::<usize>().ok()?;
179 if shared > previous_path.len() {
180 return None;
181 }
182 let path = format!("{}{}", &previous_path[..shared], parts[6]);
183 previous_path.clear();
184 previous_path.push_str(&path);
185
186 let kind = match parts[1] {
187 "d" => FileKind::Directory,
188 "s" => FileKind::Symlink,
189 _ => FileKind::Regular,
190 };
191
192 Some(FileRecord {
193 path,
194 packages: vec![package.to_owned()],
195 size: parts[2].parse().unwrap_or(0),
196 kind,
197 executable: parts[3] == "1",
198 target: parts[4].to_owned(),
199 })
200}