1use anyhow::{Context, Result, bail};
2use camino::{Utf8Path, Utf8PathBuf};
3use std::io::{BufRead, Read};
4
5use crate::*;
6
7pub(crate) const QUERYFORMAT: &str = concat!(
13 r"@@PKG@@\t%{NAME}\t%{VERSION}\t%{RELEASE}\t%{EPOCH}\t%{ARCH}",
15 r"\t%{LICENSE}\t%{SIZE}\t%{BUILDTIME}\t%{INSTALLTIME}",
16 r"\t%{SOURCERPM}\t%{FILEDIGESTALGO}\n",
17 r"[@@FILE@@\t%{FILENAMES}\t%{FILESIZES}\t%{FILEMODES}\t%{FILEMTIMES}",
19 r"\t%{FILEDIGESTS}\t%{FILEFLAGS}",
20 r"\t%{FILEUSERNAME}\t%{FILEGROUPNAME}\t%{FILELINKTOS}\n]",
21 r"[@@CL@@\t%{CHANGELOGTIME}\n]",
23);
24
25const PKG_FIELDS: usize = 11;
27const FILE_FIELDS: usize = 9;
29
30pub(crate) fn load_from_reader_impl<R: Read>(reader: R) -> Result<Packages> {
32 let mut packages = Packages::new();
33 let mut current_pkg: Option<Package> = None;
34 let mut skip = false;
36
37 for (line_no, line) in std::io::BufReader::new(reader).lines().enumerate() {
38 let line = line.context("reading line")?;
39 if line.is_empty() {
40 continue;
41 }
42
43 if let Some(rest) = line.strip_prefix("@@PKG@@\t") {
44 if let Some(pkg) = current_pkg.take() {
46 packages.insert(pkg.name.clone(), pkg);
47 }
48
49 let fields: Vec<&str> = rest.split('\t').collect();
50 if fields.len() != PKG_FIELDS {
51 bail!(
52 "line {}: expected {PKG_FIELDS} fields in PKG line, got {}",
53 line_no + 1,
54 fields.len()
55 );
56 }
57
58 let name = fields[0];
59 if name == "gpg-pubkey" {
61 skip = true;
62 continue;
63 }
64
65 skip = false;
66 let pkg = parse_pkg_header(&fields)
67 .with_context(|| format!("parsing package header at line {}", line_no + 1))?;
68 current_pkg = Some(pkg);
69 } else if skip {
70 continue;
72 } else if let Some(rest) = line.strip_prefix("@@FILE@@\t") {
73 let pkg = current_pkg
74 .as_mut()
75 .ok_or_else(|| anyhow::anyhow!("line {}: FILE line before any PKG", line_no + 1))?;
76 let fields: Vec<&str> = rest.split('\t').collect();
77 if fields.len() != FILE_FIELDS {
78 bail!(
79 "line {}: expected {} fields in FILE line for '{}', got {}",
80 line_no + 1,
81 FILE_FIELDS,
82 pkg.name,
83 fields.len()
84 );
85 }
86 let (path, info) = parse_file_line(&fields)
87 .with_context(|| format!("line {}: file in '{}'", line_no + 1, pkg.name))?;
88 pkg.files.insert(path, info);
89 } else if let Some(rest) = line.strip_prefix("@@CL@@\t") {
90 let pkg = current_pkg
91 .as_mut()
92 .ok_or_else(|| anyhow::anyhow!("line {}: CL line before any PKG", line_no + 1))?;
93 let time: u64 = rest.parse().with_context(|| {
94 format!(
95 "line {}: invalid changelog time for '{}'",
96 line_no + 1,
97 pkg.name
98 )
99 })?;
100 pkg.changelog_times.push(time);
101 } else {
102 bail!(
103 "line {}: unexpected line format: {}",
104 line_no + 1,
105 &line[..line.len().min(80)]
106 );
107 }
108 }
109
110 if let Some(pkg) = current_pkg.take() {
112 packages.insert(pkg.name.clone(), pkg);
113 }
114
115 Ok(packages)
116}
117
118pub(crate) fn load_from_str_impl(input: &str) -> Result<Packages> {
120 load_from_reader_impl(input.as_bytes())
121}
122
123fn parse_pkg_header(fields: &[&str]) -> Result<Package> {
126 assert_eq!(fields.len(), PKG_FIELDS); let name = fields[0];
128 let epoch = match parse_optional(fields[3]) {
129 None => None,
130 Some(s) => Some(
131 s.parse::<u32>()
132 .with_context(|| format!("{name}: invalid epoch '{s}'"))?,
133 ),
134 };
135 let arch = parse_optional(fields[4])
136 .ok_or_else(|| anyhow::anyhow!("{name}: missing arch"))?
137 .to_string();
138 let size = fields[6]
139 .parse::<u64>()
140 .with_context(|| format!("{name}: invalid size"))?;
141 let buildtime = fields[7]
142 .parse::<u64>()
143 .with_context(|| format!("{name}: invalid buildtime"))?;
144 let installtime = fields[8]
145 .parse::<u64>()
146 .with_context(|| format!("{name}: invalid installtime"))?;
147 let sourcerpm = parse_optional(fields[9]).map(|s| s.to_string());
148
149 let digest_algo = match parse_optional(fields[10]) {
150 None => None,
151 Some(s) => {
152 let v = s
153 .parse::<u32>()
154 .with_context(|| format!("{name}: invalid filedigestalgo '{s}'"))?;
155 Some(
156 DigestAlgorithm::try_from(v)
157 .map_err(|_| anyhow::anyhow!("{name}: unknown digest algorithm {v}"))?,
158 )
159 }
160 };
161
162 Ok(Package {
163 name: name.to_string(),
164 version: fields[1].to_string(),
165 release: fields[2].to_string(),
166 epoch,
167 arch,
168 license: fields[5].to_string(),
169 size,
170 buildtime,
171 installtime,
172 sourcerpm,
173 digest_algo,
174 changelog_times: Vec::new(),
175 files: Files::new(),
176 })
177}
178
179fn parse_optional(s: &str) -> Option<&str> {
181 if s == "(none)" { None } else { Some(s) }
182}
183
184impl TryFrom<u32> for DigestAlgorithm {
185 type Error = ();
186
187 fn try_from(v: u32) -> Result<Self, Self::Error> {
188 match v {
189 x if x == Self::Md5 as u32 => Ok(Self::Md5),
190 x if x == Self::Sha1 as u32 => Ok(Self::Sha1),
191 x if x == Self::RipeMd160 as u32 => Ok(Self::RipeMd160),
192 x if x == Self::Md2 as u32 => Ok(Self::Md2),
193 x if x == Self::Tiger192 as u32 => Ok(Self::Tiger192),
194 x if x == Self::Haval5160 as u32 => Ok(Self::Haval5160),
195 x if x == Self::Sha256 as u32 => Ok(Self::Sha256),
196 x if x == Self::Sha384 as u32 => Ok(Self::Sha384),
197 x if x == Self::Sha512 as u32 => Ok(Self::Sha512),
198 x if x == Self::Sha224 as u32 => Ok(Self::Sha224),
199 x if x == Self::Sha3_256 as u32 => Ok(Self::Sha3_256),
200 x if x == Self::Sha3_512 as u32 => Ok(Self::Sha3_512),
201 _ => Err(()),
202 }
203 }
204}
205
206fn parse_file_line(fields: &[&str]) -> Result<(Utf8PathBuf, FileInfo)> {
208 assert_eq!(fields.len(), FILE_FIELDS); let path = Utf8Path::new(fields[0]);
210 let size = fields[1]
211 .parse::<u64>()
212 .with_context(|| format!("invalid filesize for {path}"))?;
213 let mode = fields[2]
214 .parse::<u16>()
215 .with_context(|| format!("invalid filemode for {path}"))?;
216 let mtime = fields[3]
217 .parse::<u64>()
218 .with_context(|| format!("invalid filemtime for {path}"))?;
219 let digest = if fields[4].is_empty() {
220 None
221 } else {
222 Some(fields[4].to_string())
223 };
224 let flags = fields[5]
225 .parse::<u32>()
226 .with_context(|| format!("invalid fileflags for {path}"))?;
227 let linkto = if fields[8].is_empty() {
228 None
229 } else {
230 Some(Utf8PathBuf::from(fields[8]))
231 };
232
233 let info = FileInfo {
234 size,
235 mode,
236 mtime,
237 digest,
238 flags: FileFlags::from_raw(flags),
239 user: fields[6].to_string(),
240 group: fields[7].to_string(),
241 linkto,
242 };
243
244 Ok((path.to_path_buf(), info))
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 fn make_pkg_line(name: &str) -> String {
252 format!(
253 "@@PKG@@\t{name}\t1.0\t1.fc42\t(none)\tx86_64\tMIT\t100\t1000\t2000\tfoo.src.rpm\t8\n"
254 )
255 }
256
257 fn make_file_line(path: &str) -> String {
258 format!("@@FILE@@\t{path}\t100\t33188\t1000\taabbccdd\t0\troot\troot\t\n")
259 }
260
261 #[test]
262 fn test_empty_input() {
263 let packages = load_from_str_impl("").unwrap();
264 assert!(packages.is_empty());
265 }
266
267 #[test]
268 fn test_gpg_pubkey_skipped() {
269 let input =
270 "@@PKG@@\tgpg-pubkey\t1.0\t1.fc42\t(none)\t(none)\tpubkey\t0\t0\t0\t(none)\t(none)\n";
271 let packages = load_from_str_impl(input).unwrap();
272 assert!(packages.is_empty());
273 }
274
275 #[test]
276 fn test_none_optional_fields() {
277 let input = "@@PKG@@\ttest\t1.0\t1\t(none)\tx86_64\tMIT\t0\t0\t0\t(none)\t(none)\n";
278 let packages = load_from_str_impl(input).unwrap();
279 assert_eq!(packages["test"].epoch, None);
280 assert_eq!(packages["test"].sourcerpm, None);
281 assert_eq!(packages["test"].digest_algo, None);
282 assert!(packages["test"].files.is_empty());
283 assert!(packages["test"].changelog_times.is_empty());
284 }
285
286 #[test]
287 fn test_no_files_with_changelog() {
288 let mut input = make_pkg_line("test");
289 input.push_str("@@CL@@\t3000\n");
290 input.push_str("@@CL@@\t2000\n");
291 input.push_str("@@CL@@\t1000\n");
292 let packages = load_from_str_impl(&input).unwrap();
293 assert!(packages["test"].files.is_empty());
294 assert_eq!(packages["test"].changelog_times, vec![3000, 2000, 1000]);
295 }
296
297 #[test]
298 fn test_with_files_no_changelog() {
299 let mut input = make_pkg_line("test");
300 input.push_str(&make_file_line("/usr/bin/foo"));
301 input.push_str(&make_file_line("/usr/bin/bar"));
302 let packages = load_from_str_impl(&input).unwrap();
303 assert_eq!(packages["test"].files.len(), 2);
304 assert!(packages["test"].changelog_times.is_empty());
305 }
306
307 #[test]
308 fn test_with_files_and_changelog() {
309 let mut input = make_pkg_line("test");
310 input.push_str(&make_file_line("/usr/bin/foo"));
311 input.push_str(&make_file_line("/usr/bin/bar"));
312 input.push_str("@@CL@@\t2000\n");
313 input.push_str("@@CL@@\t1000\n");
314 let packages = load_from_str_impl(&input).unwrap();
315 assert_eq!(packages["test"].files.len(), 2);
316 assert_eq!(packages["test"].changelog_times, vec![2000, 1000]);
317 }
318
319 #[test]
320 fn test_multiple_packages() {
321 let mut input = make_pkg_line("alpha");
322 input.push_str(&make_file_line("/usr/bin/alpha"));
323 input.push_str(&make_pkg_line("beta"));
324 input.push_str(&make_file_line("/usr/bin/beta"));
325 let packages = load_from_str_impl(&input).unwrap();
326 assert_eq!(packages.len(), 2);
327 assert!(packages.contains_key("alpha"));
328 assert!(packages.contains_key("beta"));
329 }
330
331 #[test]
332 fn test_error_conditions() {
333 assert!(load_from_str_impl("@@PKG@@\tfoo\t1.0\n").is_err());
335
336 let mut input = make_pkg_line("test");
338 input.push_str("@@FILE@@\t/a\t0\n");
339 assert!(load_from_str_impl(&input).is_err());
340
341 assert!(load_from_str_impl("@@FILE@@\t/a\t0\t33188\t0\t\t0\troot\troot\t\n").is_err());
343
344 assert!(load_from_str_impl("garbage\n").is_err());
346 }
347
348 #[test]
349 fn test_symlink_and_empty_digest() {
350 let mut input = make_pkg_line("test");
351 input.push_str("@@FILE@@\t/usr/bin/sh\t4\t41471\t1000\t\t0\troot\troot\tbash\n");
353 let packages = load_from_str_impl(&input).unwrap();
354 let sh = &packages["test"].files[Utf8Path::new("/usr/bin/sh")];
355 assert!(sh.digest.is_none());
356 assert_eq!(sh.linkto.as_deref(), Some(Utf8Path::new("bash")));
357 }
358}