1use std::collections::BTreeMap;
11use std::io;
12use std::path::Path;
13
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct Package {
17 pub name: String,
18 pub version: String,
19 pub depends: Vec<String>,
20 pub optdepends: Vec<String>,
21 pub provides: Vec<String>,
22 pub files: Vec<String>,
24}
25
26#[derive(Debug, Default)]
28pub struct Database {
29 packages: BTreeMap<String, Package>,
30 providers: BTreeMap<String, Vec<String>>,
31 owners: BTreeMap<String, String>,
32}
33
34pub const LOCAL_DB: &str = "var/lib/pacman/local";
36
37fn invalid(msg: String) -> io::Error {
38 io::Error::new(io::ErrorKind::InvalidData, msg)
39}
40
41impl Database {
42 pub fn read(root: &Path) -> io::Result<Self> {
44 let dir = root.join(LOCAL_DB);
45 let mut db = Database::default();
46 let mut entries: Vec<_> = std::fs::read_dir(&dir)
47 .map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", dir.display())))?
48 .filter_map(Result::ok)
49 .map(|e| e.path())
50 .filter(|p| p.is_dir())
51 .collect();
52 entries.sort();
53 for entry in entries {
54 let package = read_package(&entry)?;
55 db.insert(package);
56 }
57 Ok(db)
58 }
59
60 pub fn insert(&mut self, package: Package) {
63 for provided in &package.provides {
64 self.providers
65 .entry(provided.clone())
66 .or_default()
67 .push(package.name.clone());
68 }
69 for file in &package.files {
70 self.owners
71 .entry(file.clone())
72 .or_insert_with(|| package.name.clone());
73 }
74 self.packages.insert(package.name.clone(), package);
75 }
76
77 pub fn package(&self, name: &str) -> Option<&Package> {
78 self.packages.get(name)
79 }
80
81 pub fn packages(&self) -> impl Iterator<Item = &Package> {
82 self.packages.values()
83 }
84
85 pub fn owner(&self, path: &str) -> Option<&Package> {
87 let path = path.trim_start_matches('/');
88 self.owners
89 .get(path)
90 .and_then(|name| self.packages.get(name))
91 }
92
93 pub fn satisfier(&self, name: &str) -> Option<&Package> {
96 if let Some(p) = self.packages.get(name) {
97 return Some(p);
98 }
99 self.providers
100 .get(name)
101 .and_then(|list| list.iter().find_map(|n| self.packages.get(n)))
102 }
103}
104
105fn read_package(dir: &Path) -> io::Result<Package> {
106 let context = |what: &str| format!("{}: {what}", dir.display());
107 let desc = std::fs::read_to_string(dir.join("desc"))
108 .map_err(|e| io::Error::new(e.kind(), context(&format!("desc: {e}"))))?;
109 let sections = parse_sections(&desc);
110 let name = sections
111 .get("NAME")
112 .and_then(|v| v.first())
113 .cloned()
114 .ok_or_else(|| invalid(context("desc has no %NAME%")))?;
115 let version = sections
116 .get("VERSION")
117 .and_then(|v| v.first())
118 .cloned()
119 .ok_or_else(|| invalid(context("desc has no %VERSION%")))?;
120 let names = |key: &str| -> Vec<String> {
121 sections
122 .get(key)
123 .map(|v| v.iter().map(|s| dependency_name(s)).collect())
124 .unwrap_or_default()
125 };
126
127 let files_text = std::fs::read_to_string(dir.join("files"))
128 .map_err(|e| io::Error::new(e.kind(), context(&format!("files: {e}"))))?;
129 let files = parse_sections(&files_text)
130 .remove("FILES")
131 .unwrap_or_default()
132 .into_iter()
133 .filter(|f| !f.ends_with('/'))
134 .collect();
135
136 Ok(Package {
137 name,
138 version,
139 depends: names("DEPENDS"),
140 optdepends: names("OPTDEPENDS"),
141 provides: names("PROVIDES"),
142 files,
143 })
144}
145
146fn parse_sections(text: &str) -> BTreeMap<String, Vec<String>> {
149 let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
150 let mut current: Option<String> = None;
151 for line in text.lines() {
152 let line = line.trim_end();
153 if line.len() > 2 && line.starts_with('%') && line.ends_with('%') {
154 let key = line[1..line.len() - 1].to_string();
155 out.entry(key.clone()).or_default();
156 current = Some(key);
157 } else if line.is_empty() {
158 current = None;
159 } else if let Some(key) = ¤t {
160 out.get_mut(key).unwrap().push(line.to_string());
161 }
162 }
163 out
164}
165
166pub fn dependency_name(entry: &str) -> String {
170 let end = entry.find(['<', '>', '=', ':']).unwrap_or(entry.len());
171 entry[..end].trim().to_string()
172}
173
174#[cfg(test)]
175pub(crate) mod fixture {
176 use super::*;
177
178 pub(crate) fn write_package(root: &Path, package: &Package, dirs: &[&str]) {
180 let dir = root
181 .join(LOCAL_DB)
182 .join(format!("{}-{}", package.name, package.version));
183 std::fs::create_dir_all(&dir).unwrap();
184 let mut desc = format!(
185 "%NAME%\n{}\n\n%VERSION%\n{}\n\n",
186 package.name, package.version
187 );
188 for (key, list) in [
189 ("DEPENDS", &package.depends),
190 ("OPTDEPENDS", &package.optdepends),
191 ("PROVIDES", &package.provides),
192 ] {
193 if !list.is_empty() {
194 desc.push_str(&format!("%{key}%\n{}\n\n", list.join("\n")));
195 }
196 }
197 std::fs::write(dir.join("desc"), desc).unwrap();
198 let mut files = String::from("%FILES%\n");
199 for d in dirs {
200 files.push_str(d);
201 files.push_str("/\n");
202 }
203 for f in &package.files {
204 files.push_str(f);
205 files.push('\n');
206 }
207 files.push_str("\n%BACKUP%\n");
208 std::fs::write(dir.join("files"), files).unwrap();
209 }
210
211 pub(crate) struct TempRoot(std::path::PathBuf);
213
214 impl std::ops::Deref for TempRoot {
215 type Target = std::path::Path;
216 fn deref(&self) -> &std::path::Path {
217 &self.0
218 }
219 }
220
221 impl AsRef<std::path::Path> for TempRoot {
222 fn as_ref(&self) -> &std::path::Path {
223 &self.0
224 }
225 }
226
227 impl Drop for TempRoot {
228 fn drop(&mut self) {
229 let _ = std::fs::remove_dir_all(&self.0);
230 }
231 }
232
233 pub(crate) fn temp_root(tag: &str) -> TempRoot {
234 let dir = std::env::temp_dir().join(format!(
235 "onelf-sysroot-{tag}-{}-{}",
236 std::process::id(),
237 std::time::SystemTime::now()
238 .duration_since(std::time::UNIX_EPOCH)
239 .unwrap()
240 .as_nanos()
241 ));
242 std::fs::create_dir_all(&dir).unwrap();
243 TempRoot(dir)
244 }
245
246 pub(crate) fn pkg(name: &str, files: &[&str]) -> Package {
247 Package {
248 name: name.into(),
249 version: "1.0-1".into(),
250 files: files.iter().map(|s| s.to_string()).collect(),
251 ..Default::default()
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::fixture::*;
259 use super::*;
260
261 #[test]
262 fn dependency_names_drop_constraints_and_descriptions() {
263 assert_eq!(dependency_name("glibc>=2.39"), "glibc");
264 assert_eq!(dependency_name("libfoo.so=1-64"), "libfoo.so");
265 assert_eq!(dependency_name("mesa: OpenGL support"), "mesa");
266 assert_eq!(dependency_name("zlib"), "zlib");
267 }
268
269 #[test]
270 fn reads_packages_files_and_owners() {
271 let root = temp_root("db");
272 let mut app = pkg("app", &["usr/bin/app"]);
273 app.depends = vec!["libfixture>=1".into(), "libfoo.so=1-64".into()];
274 app.optdepends = vec!["extra: more".into()];
275 write_package(&root, &app, &["usr", "usr/bin"]);
276 let mut lib = pkg("libfixture", &["usr/lib/libfixture.so.1"]);
277 lib.provides = vec!["libfoo.so=1-64".into()];
278 write_package(&root, &lib, &["usr", "usr/lib"]);
279
280 let db = Database::read(&root).unwrap();
281 let app = db.package("app").unwrap();
282 assert_eq!(app.depends, ["libfixture", "libfoo.so"]);
283 assert_eq!(app.optdepends, ["extra"]);
284 assert_eq!(db.owner("/usr/bin/app").unwrap().name, "app");
285 assert_eq!(
286 db.owner("usr/lib/libfixture.so.1").unwrap().name,
287 "libfixture"
288 );
289 assert!(
290 db.owner("usr/lib").is_none(),
291 "directories are owned by no one"
292 );
293 assert_eq!(db.satisfier("libfoo.so").unwrap().name, "libfixture");
294 assert!(db.satisfier("nothing").is_none());
295 }
296
297 #[test]
298 fn a_truncated_desc_fails_naming_the_package() {
299 let root = temp_root("dbcut");
300 let dir = root.join(LOCAL_DB).join("broken-1.0-1");
301 std::fs::create_dir_all(&dir).unwrap();
302 std::fs::write(dir.join("desc"), "%NAME%\nbroken\n\n%VERSI").unwrap();
303 std::fs::write(dir.join("files"), "%FILES%\n").unwrap();
304 let err = Database::read(&root).unwrap_err();
305 assert!(err.to_string().contains("broken-1.0-1"), "{err}");
306 }
307
308 #[test]
309 fn a_missing_database_is_an_error() {
310 let root = temp_root("nodb");
311 assert!(Database::read(&root).is_err());
312 }
313}