pkgsrc/summary.rs
1/*
2 * Copyright (c) 2026 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17/*!
18 * Parsing and generation of [`pkg_summary(5)`] package metadata.
19 *
20 * A `pkg_summary` file contains a selection of useful package metadata, and is
21 * primarily used by binary package managers to retrieve information about a
22 * package repository.
23 *
24 * A complete package entry contains a list of `VARIABLE=VALUE` pairs,
25 * and a complete `pkg_summary` file consists of multiple package entries
26 * separated by single blank lines.
27 *
28 * A [`Summary`] can be created in two ways:
29 *
30 * * **Parsing**: Use [`Summary::from_reader`] to parse multiple entries from
31 * any [`BufRead`] source, returning an iterator over [`Result<Summary>`].
32 * Single entries can be parsed using [`FromStr`].
33 *
34 * * **Building**: Use [`SummaryBuilder::new`], add `VARIABLE=VALUE` lines with
35 * [`SummaryBuilder::vars`], then call [`SummaryBuilder::build`] to validate
36 * and construct the entry.
37 *
38 * Parsing operations return [`enum@Error`] on failure. Each error variant
39 * includes span information for use with pretty-printing error reporting
40 * libraries such as [`ariadne`] or [`miette`] which can be helpful to show
41 * exact locations of errors.
42 *
43 * Once validated, [`Summary`] provides many access [`methods`] to retrieve
44 * information about each variable in a summary entry.
45 *
46 * ## Examples
47 *
48 * Read [`pkg_summary.gz`] and print list of packages in `pkg_info` format,
49 * similar to how `pkgin avail` works.
50 *
51 * ```
52 * use flate2::read::GzDecoder;
53 * use pkgsrc::summary::Summary;
54 * use std::fs::File;
55 * use std::io::BufReader;
56 *
57 * # fn main() -> Result<(), Box<dyn std::error::Error>> {
58 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
59 * let file = File::open(path).expect("failed to open pkg_summary.gz");
60 * let reader = BufReader::new(GzDecoder::new(file));
61 *
62 * for pkg in Summary::from_reader(reader) {
63 * let pkg = pkg?;
64 * println!("{:20} {}", pkg.pkgname(), pkg.comment());
65 * }
66 * # Ok(())
67 * # }
68 * ```
69 *
70 * Create a [`Summary`] entry 4 different ways from an input file containing `pkg_summary`
71 * data for `mktool-1.4.2` extracted from the main [`pkg_summary.gz`].
72 *
73 * ```
74 * use pkgsrc::summary::{Summary, SummaryBuilder};
75 * use std::str::FromStr;
76 *
77 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/mktool.txt");
78 * let input = std::fs::read_to_string(path).expect("failed to read mktool.txt");
79 *
80 * // Parse implicitly through FromStr's parse
81 * assert_eq!(
82 * input.parse::<Summary>().expect("parse failed").pkgname(),
83 * "mktool-1.4.2"
84 * );
85 *
86 * // Parse explicitly via from_str
87 * assert_eq!(
88 * Summary::from_str(&input).expect("from_str failed").pkgname(),
89 * "mktool-1.4.2"
90 * );
91 *
92 * // Use the builder pattern, passing all input through a single vars() call.
93 * assert_eq!(
94 * SummaryBuilder::new()
95 * .vars(input.lines())
96 * .build()
97 * .expect("build failed")
98 * .pkgname(),
99 * "mktool-1.4.2"
100 * );
101 *
102 * // Use the builder pattern but build up the input with separate var() calls.
103 * let mut builder = SummaryBuilder::new();
104 * for line in input.lines() {
105 * builder = builder.var(line);
106 * }
107 * assert_eq!(builder.build().expect("build failed").pkgname(), "mktool-1.4.2");
108 * ```
109 *
110 * [`BufRead`]: std::io::BufRead
111 * [`ariadne`]: https://docs.rs/ariadne
112 * [`methods`]: Summary#implementations
113 * [`miette`]: https://docs.rs/miette
114 * [`pkg_summary(5)`]: https://man.netbsd.org/pkg_summary.5
115 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
116 *
117 */
118use std::fmt;
119use std::io::{self, BufRead};
120use std::num::ParseIntError;
121use std::str::FromStr;
122
123use crate::PkgName;
124use crate::kv::Kv;
125
126pub use crate::kv::Span;
127
128/// Error context containing optional entry number and span information.
129#[derive(Clone, Debug, Default, PartialEq, Eq)]
130#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
131pub struct ErrorContext {
132 entry: Option<usize>,
133 span: Option<Span>,
134}
135
136impl ErrorContext {
137 /// Create a new error context with the given span.
138 #[must_use]
139 pub fn new(span: Span) -> Self {
140 Self {
141 entry: None,
142 span: Some(span),
143 }
144 }
145
146 /// Add entry number to this context.
147 #[must_use]
148 pub fn with_entry(mut self, entry: usize) -> Self {
149 self.entry = Some(entry);
150 self
151 }
152
153 /// Adjust the span offset by adding the given amount.
154 #[must_use]
155 pub fn adjust_offset(mut self, adjustment: usize) -> Self {
156 if let Some(ref mut span) = self.span {
157 span.offset += adjustment;
158 }
159 self
160 }
161
162 /// Set span if not already set.
163 #[must_use]
164 pub fn with_span_if_none(mut self, span: Span) -> Self {
165 if self.span.is_none() {
166 self.span = Some(span);
167 }
168 self
169 }
170
171 /// Return the entry number if set.
172 #[must_use]
173 pub const fn entry(&self) -> Option<usize> {
174 self.entry
175 }
176
177 /// Return the span if set.
178 #[must_use]
179 pub const fn span(&self) -> Option<Span> {
180 self.span
181 }
182}
183
184#[cfg(test)]
185use indoc::indoc;
186
187/**
188 * A type alias for the result from parsing a [`Summary`], with
189 * [`enum@Error`] returned in [`Err`] variants.
190 */
191pub type Result<T> = std::result::Result<T, Error>;
192
193/*
194 * Note that (as far as my reading of it suggests) we cannot return an error
195 * via fmt::Result if there are any issues with missing fields, so we can only
196 * print what we have and validation will have to occur elsewhere.
197 */
198impl fmt::Display for Summary {
199 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
200 macro_rules! write_required_field {
201 ($field:expr, $name:expr) => {
202 writeln!(f, "{}={}", $name, $field)?;
203 };
204 }
205
206 macro_rules! write_optional_field {
207 ($field:expr, $name:expr) => {
208 if let Some(val) = &$field {
209 writeln!(f, "{}={}", $name, val)?;
210 }
211 };
212 }
213
214 macro_rules! write_required_array_field {
215 ($field:expr, $name:expr) => {
216 for val in $field {
217 writeln!(f, "{}={}", $name, val)?;
218 }
219 };
220 }
221
222 macro_rules! write_optional_array_field {
223 ($field:expr, $name:expr) => {
224 if let Some(arr) = &$field {
225 for val in arr {
226 writeln!(f, "{}={}", $name, val)?;
227 }
228 }
229 };
230 }
231
232 /* Retain compatible output ordering with pkg_info(1) */
233 write_optional_array_field!(self.conflicts, "CONFLICTS");
234 write_required_field!(self.pkgname.pkgname(), "PKGNAME");
235 write_optional_array_field!(self.depends, "DEPENDS");
236 write_required_field!(&self.comment, "COMMENT");
237 write_required_field!(self.size_pkg, "SIZE_PKG");
238 write_required_field!(&self.build_date, "BUILD_DATE");
239 writeln!(f, "CATEGORIES={}", self.categories.join(" "))?;
240 write_optional_field!(self.homepage, "HOMEPAGE");
241 write_optional_field!(self.license, "LICENSE");
242 write_required_field!(&self.machine_arch, "MACHINE_ARCH");
243 write_required_field!(&self.opsys, "OPSYS");
244 write_required_field!(&self.os_version, "OS_VERSION");
245 write_required_field!(&self.pkgpath, "PKGPATH");
246 write_required_field!(&self.pkgtools_version, "PKGTOOLS_VERSION");
247 write_optional_field!(self.pkg_options, "PKG_OPTIONS");
248 write_optional_field!(self.prev_pkgpath, "PREV_PKGPATH");
249 write_optional_array_field!(self.provides, "PROVIDES");
250 write_optional_array_field!(self.requires, "REQUIRES");
251 write_optional_array_field!(self.supersedes, "SUPERSEDES");
252 write_optional_field!(self.file_name, "FILE_NAME");
253 write_optional_field!(self.file_size, "FILE_SIZE");
254 write_optional_field!(self.file_cksum, "FILE_CKSUM");
255 // Always output at least one DESCRIPTION= line, even if empty
256 if self.description.is_empty() {
257 writeln!(f, "DESCRIPTION=")?;
258 } else {
259 write_required_array_field!(&self.description, "DESCRIPTION");
260 }
261
262 Ok(())
263 }
264}
265
266/**
267 * A single [`pkg_summary(5)`] entry.
268 *
269 * See the [module-level documentation](self) for usage examples.
270 *
271 * [`pkg_summary(5)`]: https://man.netbsd.org/pkg_summary.5
272 */
273#[derive(Clone, Debug, PartialEq, Eq, Kv)]
274pub struct Summary {
275 #[kv(variable = "BUILD_DATE")]
276 build_date: String,
277
278 #[kv(variable = "CATEGORIES")]
279 categories: Vec<String>,
280
281 #[kv(variable = "COMMENT")]
282 comment: String,
283
284 #[kv(variable = "CONFLICTS", multiline)]
285 conflicts: Option<Vec<String>>,
286
287 #[kv(variable = "DEPENDS", multiline)]
288 depends: Option<Vec<String>>,
289
290 #[kv(variable = "DESCRIPTION", multiline)]
291 description: Vec<String>,
292
293 #[kv(variable = "FILE_CKSUM")]
294 file_cksum: Option<String>,
295
296 #[kv(variable = "FILE_NAME")]
297 file_name: Option<String>,
298
299 #[kv(variable = "FILE_SIZE")]
300 file_size: Option<u64>,
301
302 #[kv(variable = "HOMEPAGE")]
303 homepage: Option<String>,
304
305 #[kv(variable = "LICENSE")]
306 license: Option<String>,
307
308 #[kv(variable = "MACHINE_ARCH")]
309 machine_arch: String,
310
311 #[kv(variable = "OPSYS")]
312 opsys: String,
313
314 #[kv(variable = "OS_VERSION")]
315 os_version: String,
316
317 #[kv(variable = "PKGNAME")]
318 pkgname: PkgName,
319
320 #[kv(variable = "PKGPATH")]
321 pkgpath: String,
322
323 #[kv(variable = "PKGTOOLS_VERSION")]
324 pkgtools_version: String,
325
326 #[kv(variable = "PKG_OPTIONS")]
327 pkg_options: Option<String>,
328
329 #[kv(variable = "PREV_PKGPATH")]
330 prev_pkgpath: Option<String>,
331
332 #[kv(variable = "PROVIDES", multiline)]
333 provides: Option<Vec<String>>,
334
335 #[kv(variable = "REQUIRES", multiline)]
336 requires: Option<Vec<String>>,
337
338 #[kv(variable = "SIZE_PKG")]
339 size_pkg: u64,
340
341 #[kv(variable = "SUPERSEDES", multiline)]
342 supersedes: Option<Vec<String>>,
343}
344
345/**
346 * Builder for constructing a [`Summary`] from `VARIABLE=VALUE` lines.
347 *
348 * ## Example
349 *
350 * ```
351 * use pkgsrc::summary::SummaryBuilder;
352 *
353 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/mktool.txt");
354 * let input = std::fs::read_to_string(path).expect("failed to read mktool.txt");
355 *
356 * assert_eq!(
357 * SummaryBuilder::new()
358 * .vars(input.lines())
359 * .build()
360 * .expect("build failed")
361 * .pkgname(),
362 * "mktool-1.4.2"
363 * );
364 * ```
365 */
366#[derive(Clone, Debug, Default, Eq, PartialEq)]
367#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
368pub struct SummaryBuilder {
369 lines: Vec<String>,
370 allow_unknown: bool,
371 allow_incomplete: bool,
372}
373
374impl SummaryBuilder {
375 /**
376 * Create a new empty [`SummaryBuilder`].
377 */
378 #[must_use]
379 pub fn new() -> Self {
380 Self::default()
381 }
382
383 /**
384 * Add a single `VARIABLE=VALUE` line.
385 *
386 * This method is infallible; validation occurs when [`build`] is called.
387 *
388 * Prefer [`vars`] when adding multiple variables.
389 *
390 * [`vars`]: SummaryBuilder::vars
391 * [`build`]: SummaryBuilder::build
392 */
393 #[must_use]
394 pub fn var(mut self, line: impl AsRef<str>) -> Self {
395 self.lines.push(line.as_ref().to_string());
396 self
397 }
398
399 /**
400 * Add `VARIABLE=VALUE` lines.
401 *
402 * This method is infallible; validation occurs when [`build`] is called.
403 *
404 * ## Example
405 *
406 * ```
407 * use pkgsrc::summary::SummaryBuilder;
408 *
409 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/mktool.txt");
410 * let input = std::fs::read_to_string(path).expect("failed to read mktool.txt");
411 *
412 * assert_eq!(
413 * SummaryBuilder::new()
414 * .vars(input.lines())
415 * .build()
416 * .expect("build failed")
417 * .pkgname(),
418 * "mktool-1.4.2"
419 * );
420 * ```
421 *
422 * [`build`]: SummaryBuilder::build
423 */
424 #[must_use]
425 pub fn vars<I, S>(mut self, lines: I) -> Self
426 where
427 I: IntoIterator<Item = S>,
428 S: AsRef<str>,
429 {
430 for line in lines {
431 self.lines.push(line.as_ref().to_string());
432 }
433 self
434 }
435
436 /// Allow unknown variables instead of returning an error.
437 #[must_use]
438 pub fn allow_unknown(mut self, yes: bool) -> Self {
439 self.allow_unknown = yes;
440 self
441 }
442
443 /// Allow incomplete entries missing required fields.
444 #[must_use]
445 pub fn allow_incomplete(mut self, yes: bool) -> Self {
446 self.allow_incomplete = yes;
447 self
448 }
449
450 /**
451 * Validate and finalize the [`Summary`].
452 *
453 * Parses all added variables, validates that all required fields are
454 * present, and returns the completed [`Summary`].
455 *
456 * ## Errors
457 *
458 * Returns [`Error`] if the input is invalid. Applications may want to
459 * ignore [`Error::UnknownVariable`] if they wish to be future-proof
460 * against potential new additions to the `pkg_summary` format.
461 *
462 * ## Examples
463 *
464 * ```
465 * use pkgsrc::summary::{Error, SummaryBuilder};
466 *
467 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/mktool.txt");
468 * let input = std::fs::read_to_string(path).expect("failed to read mktool.txt");
469 *
470 * // Valid pkg_summary data.
471 * assert_eq!(
472 * SummaryBuilder::new()
473 * .vars(input.lines())
474 * .build()
475 * .expect("build failed")
476 * .pkgname(),
477 * "mktool-1.4.2"
478 * );
479 *
480 * // Missing required fields.
481 * assert!(matches!(
482 * SummaryBuilder::new()
483 * .vars(["PKGNAME=testpkg-1.0", "COMMENT=Test"])
484 * .build(),
485 * Err(Error::Incomplete { .. })
486 * ));
487 *
488 * // Contains a line not in VARIABLE=VALUE format.
489 * assert!(matches!(
490 * SummaryBuilder::new()
491 * .vars(["not a valid line"])
492 * .build(),
493 * Err(Error::ParseLine { .. })
494 * ));
495 *
496 * // Unknown variable name.
497 * assert!(matches!(
498 * SummaryBuilder::new()
499 * .vars(["UNKNOWN=value"])
500 * .build(),
501 * Err(Error::UnknownVariable { .. })
502 * ));
503 *
504 * // Invalid integer value (with all other required fields present).
505 * assert!(matches!(
506 * SummaryBuilder::new()
507 * .vars([
508 * "BUILD_DATE=2019-08-12",
509 * "CATEGORIES=devel",
510 * "COMMENT=test",
511 * "DESCRIPTION=test",
512 * "MACHINE_ARCH=x86_64",
513 * "OPSYS=NetBSD",
514 * "OS_VERSION=9.0",
515 * "PKGNAME=test-1.0",
516 * "PKGPATH=devel/test",
517 * "PKGTOOLS_VERSION=20091115",
518 * "SIZE_PKG=not_a_number",
519 * ])
520 * .build(),
521 * Err(Error::ParseInt { .. })
522 * ));
523 * ```
524 */
525 pub fn build(self) -> Result<Summary> {
526 let input = self.lines.join("\n");
527 parse_summary(&input, self.allow_unknown, self.allow_incomplete)
528 }
529}
530
531impl Summary {
532 /// Create a new Summary with all fields.
533 #[allow(clippy::too_many_arguments)]
534 #[must_use]
535 pub(crate) fn new(
536 pkgname: PkgName,
537 comment: String,
538 size_pkg: u64,
539 build_date: String,
540 categories: Vec<String>,
541 machine_arch: String,
542 opsys: String,
543 os_version: String,
544 pkgpath: String,
545 pkgtools_version: String,
546 description: Vec<String>,
547 conflicts: Option<Vec<String>>,
548 depends: Option<Vec<String>>,
549 homepage: Option<String>,
550 license: Option<String>,
551 pkg_options: Option<String>,
552 prev_pkgpath: Option<String>,
553 provides: Option<Vec<String>>,
554 requires: Option<Vec<String>>,
555 supersedes: Option<Vec<String>>,
556 file_name: Option<String>,
557 file_size: Option<u64>,
558 file_cksum: Option<String>,
559 ) -> Self {
560 Self {
561 build_date,
562 categories,
563 comment,
564 conflicts,
565 depends,
566 description,
567 file_cksum,
568 file_name,
569 file_size,
570 homepage,
571 license,
572 machine_arch,
573 opsys,
574 os_version,
575 pkgname,
576 pkgpath,
577 pkgtools_version,
578 pkg_options,
579 prev_pkgpath,
580 provides,
581 requires,
582 size_pkg,
583 supersedes,
584 }
585 }
586
587 /**
588 * Create an iterator that parses Summary entries from a reader.
589 *
590 * ## Example
591 *
592 * ```no_run
593 * use pkgsrc::summary::Summary;
594 * use std::fs::File;
595 * use std::io::BufReader;
596 *
597 * let file = File::open("pkg_summary.txt").unwrap();
598 * let reader = BufReader::new(file);
599 *
600 * for result in Summary::from_reader(reader) {
601 * match result {
602 * Ok(summary) => println!("{}", summary.pkgname()),
603 * Err(e) => eprintln!("Error: {}", e),
604 * }
605 * }
606 * ```
607 */
608 pub fn from_reader<R: BufRead>(reader: R) -> SummaryIter<R> {
609 SummaryIter {
610 reader,
611 line_buf: String::new(),
612 buffer: String::new(),
613 record_number: 0,
614 byte_offset: 0,
615 entry_start: 0,
616 allow_unknown: false,
617 allow_incomplete: false,
618 }
619 }
620
621 /**
622 * Returns the `BUILD_DATE` value. This is a required field.
623 *
624 * ## Example
625 *
626 * Parse [`pkg_summary.gz`] and return the `BUILD_DATE` for `mktool`.
627 *
628 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
629 *
630 * ```
631 * use flate2::read::GzDecoder;
632 * use pkgsrc::summary::Summary;
633 * use std::fs::File;
634 * use std::io::BufReader;
635 *
636 * # fn main() -> std::io::Result<()> {
637 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
638 * let file = File::open(path)?;
639 * let decoder = GzDecoder::new(file);
640 * let reader = BufReader::new(decoder);
641 *
642 * let pkgs: Vec<_> = Summary::from_reader(reader)
643 * .filter_map(Result::ok)
644 * .collect();
645 *
646 * assert_eq!(
647 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
648 * .expect("mktool not found")
649 * .build_date(),
650 * "2025-11-17 22:03:08 +0000"
651 * );
652 *
653 * # Ok(())
654 * # }
655 * ```
656 */
657 pub fn build_date(&self) -> &str {
658 &self.build_date
659 }
660
661 /**
662 * Returns a [`Vec`] containing the `CATEGORIES` values. This is a
663 * required field.
664 *
665 * Note that the `CATEGORIES` field is a space-delimited string, but it
666 * makes more sense for this API to return the values as a [`Vec`].
667 *
668 * ## Example
669 *
670 * Parse [`pkg_summary.gz`] and return `CATEGORIES` for `mktool` (single
671 * category) and `9e` (multiple categories).
672 *
673 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
674 *
675 * ```
676 * use flate2::read::GzDecoder;
677 * use pkgsrc::summary::Summary;
678 * use std::fs::File;
679 * use std::io::BufReader;
680 *
681 * # fn main() -> std::io::Result<()> {
682 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
683 * let file = File::open(path)?;
684 * let decoder = GzDecoder::new(file);
685 * let reader = BufReader::new(decoder);
686 *
687 * let pkgs: Vec<_> = Summary::from_reader(reader)
688 * .filter_map(Result::ok)
689 * .collect();
690 *
691 * assert_eq!(
692 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
693 * .expect("mktool not found")
694 * .categories(),
695 * ["pkgtools"]
696 * );
697 *
698 * assert_eq!(
699 * pkgs.iter().find(|p| p.pkgname() == "9e-1.0")
700 * .expect("9e not found")
701 * .categories(),
702 * ["archivers", "plan9"]
703 * );
704 *
705 * # Ok(())
706 * # }
707 * ```
708 */
709 pub fn categories(&self) -> &[String] {
710 &self.categories
711 }
712
713 /**
714 * Returns the `COMMENT` value. This is a required field.
715 *
716 * ## Example
717 *
718 * Parse [`pkg_summary.gz`] and return the `COMMENT` for `mktool`.
719 *
720 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
721 *
722 * ```
723 * use flate2::read::GzDecoder;
724 * use pkgsrc::summary::Summary;
725 * use std::fs::File;
726 * use std::io::BufReader;
727 *
728 * # fn main() -> std::io::Result<()> {
729 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
730 * let file = File::open(path)?;
731 * let decoder = GzDecoder::new(file);
732 * let reader = BufReader::new(decoder);
733 *
734 * let pkgs: Vec<_> = Summary::from_reader(reader)
735 * .filter_map(Result::ok)
736 * .collect();
737 *
738 * assert_eq!(
739 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
740 * .expect("mktool not found")
741 * .comment(),
742 * "High performance alternatives for pkgsrc/mk"
743 * );
744 *
745 * # Ok(())
746 * # }
747 * ```
748 */
749 pub fn comment(&self) -> &str {
750 &self.comment
751 }
752
753 /**
754 * Returns a [`Vec`] containing optional `CONFLICTS` values, or [`None`]
755 * if there are none.
756 *
757 * ## Example
758 *
759 * Parse [`pkg_summary.gz`] and return `CONFLICTS` for `mktool` (none) and
760 * `angband` (multiple).
761 *
762 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
763 *
764 * ```
765 * use flate2::read::GzDecoder;
766 * use pkgsrc::summary::Summary;
767 * use std::fs::File;
768 * use std::io::BufReader;
769 *
770 * # fn main() -> std::io::Result<()> {
771 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
772 * let file = File::open(path)?;
773 * let decoder = GzDecoder::new(file);
774 * let reader = BufReader::new(decoder);
775 *
776 * let pkgs: Vec<_> = Summary::from_reader(reader)
777 * .filter_map(Result::ok)
778 * .collect();
779 *
780 * assert_eq!(
781 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
782 * .expect("mktool not found")
783 * .conflicts(),
784 * None
785 * );
786 *
787 * assert_eq!(
788 * pkgs.iter().find(|p| p.pkgname() == "angband-4.2.5nb1")
789 * .expect("angband not found")
790 * .conflicts(),
791 * Some(["angband-tty-[0-9]*", "angband-sdl-[0-9]*", "angband-x11-[0-9]*"]
792 * .map(String::from).as_slice())
793 * );
794 *
795 * # Ok(())
796 * # }
797 * ```
798 */
799 pub fn conflicts(&self) -> Option<&[String]> {
800 self.conflicts.as_deref()
801 }
802
803 /**
804 * Returns a [`Vec`] containing optional `DEPENDS` values, or [`None`]
805 * if there are none.
806 *
807 * ## Example
808 *
809 * Parse [`pkg_summary.gz`] and return `DEPENDS` for `mktool` (none) and
810 * `R-RcppTOML` (multiple).
811 *
812 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
813 *
814 * ```
815 * use flate2::read::GzDecoder;
816 * use pkgsrc::summary::Summary;
817 * use std::fs::File;
818 * use std::io::BufReader;
819 *
820 * # fn main() -> std::io::Result<()> {
821 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
822 * let file = File::open(path)?;
823 * let decoder = GzDecoder::new(file);
824 * let reader = BufReader::new(decoder);
825 *
826 * let pkgs: Vec<_> = Summary::from_reader(reader)
827 * .filter_map(Result::ok)
828 * .collect();
829 *
830 * assert_eq!(
831 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
832 * .expect("mktool not found")
833 * .depends(),
834 * None
835 * );
836 *
837 * assert_eq!(
838 * pkgs.iter().find(|p| p.pkgname() == "R-RcppTOML-0.2.2")
839 * .expect("R-RcppTOML not found")
840 * .depends(),
841 * Some(["R>=4.2.0nb1", "R-Rcpp>=1.0.2"].map(String::from).as_slice())
842 * );
843 *
844 * # Ok(())
845 * # }
846 * ```
847 */
848 pub fn depends(&self) -> Option<&[String]> {
849 self.depends.as_deref()
850 }
851
852 /**
853 * Returns a [`Vec`] containing `DESCRIPTION` values. This is a required
854 * field.
855 *
856 * ## Example
857 *
858 * Parse [`pkg_summary.gz`] and return `DESCRIPTION` for `mktool`.
859 *
860 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
861 *
862 * ```
863 * use flate2::read::GzDecoder;
864 * use pkgsrc::summary::Summary;
865 * use std::fs::File;
866 * use std::io::BufReader;
867 *
868 * # fn main() -> std::io::Result<()> {
869 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
870 * let file = File::open(path)?;
871 * let decoder = GzDecoder::new(file);
872 * let reader = BufReader::new(decoder);
873 *
874 * let pkgs: Vec<_> = Summary::from_reader(reader)
875 * .filter_map(Result::ok)
876 * .collect();
877 *
878 * // mktool's description has 20 lines
879 * assert_eq!(
880 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
881 * .expect("mktool not found")
882 * .description()
883 * .len(),
884 * 20
885 * );
886 *
887 * # Ok(())
888 * # }
889 * ```
890 */
891 pub fn description(&self) -> &[String] {
892 self.description.as_slice()
893 }
894
895 /**
896 * Returns the `FILE_CKSUM` value if set. This is an optional field.
897 *
898 * ## Example
899 *
900 * Parse [`pkg_summary.gz`] and return `FILE_CKSUM` for `mktool`.
901 *
902 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
903 *
904 * ```
905 * use flate2::read::GzDecoder;
906 * use pkgsrc::summary::Summary;
907 * use std::fs::File;
908 * use std::io::BufReader;
909 *
910 * # fn main() -> std::io::Result<()> {
911 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
912 * let file = File::open(path)?;
913 * let decoder = GzDecoder::new(file);
914 * let reader = BufReader::new(decoder);
915 *
916 * let pkgs: Vec<_> = Summary::from_reader(reader)
917 * .filter_map(Result::ok)
918 * .collect();
919 *
920 * assert_eq!(
921 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
922 * .expect("mktool not found")
923 * .file_cksum(),
924 * None
925 * );
926 *
927 * # Ok(())
928 * # }
929 * ```
930 */
931 pub fn file_cksum(&self) -> Option<&str> {
932 self.file_cksum.as_deref()
933 }
934
935 /**
936 * Returns the `FILE_NAME` value if set. This is an optional field.
937 *
938 * ## Example
939 *
940 * Parse [`pkg_summary.gz`] and return `FILE_NAME` for `mktool`.
941 *
942 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
943 *
944 * ```
945 * use flate2::read::GzDecoder;
946 * use pkgsrc::summary::Summary;
947 * use std::fs::File;
948 * use std::io::BufReader;
949 *
950 * # fn main() -> std::io::Result<()> {
951 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
952 * let file = File::open(path)?;
953 * let decoder = GzDecoder::new(file);
954 * let reader = BufReader::new(decoder);
955 *
956 * let pkgs: Vec<_> = Summary::from_reader(reader)
957 * .filter_map(Result::ok)
958 * .collect();
959 *
960 * assert_eq!(
961 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
962 * .expect("mktool not found")
963 * .file_name(),
964 * Some("mktool-1.4.2.tgz")
965 * );
966 *
967 * # Ok(())
968 * # }
969 * ```
970 */
971 pub fn file_name(&self) -> Option<&str> {
972 self.file_name.as_deref()
973 }
974
975 /**
976 * Returns the `FILE_SIZE` value if set. This is an optional field.
977 *
978 * ## Example
979 *
980 * Parse [`pkg_summary.gz`] and return `FILE_SIZE` for `mktool`.
981 *
982 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
983 *
984 * ```
985 * use flate2::read::GzDecoder;
986 * use pkgsrc::summary::Summary;
987 * use std::fs::File;
988 * use std::io::BufReader;
989 *
990 * # fn main() -> std::io::Result<()> {
991 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
992 * let file = File::open(path)?;
993 * let decoder = GzDecoder::new(file);
994 * let reader = BufReader::new(decoder);
995 *
996 * let pkgs: Vec<_> = Summary::from_reader(reader)
997 * .filter_map(Result::ok)
998 * .collect();
999 *
1000 * assert_eq!(
1001 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1002 * .expect("mktool not found")
1003 * .file_size(),
1004 * Some(2871260)
1005 * );
1006 *
1007 * # Ok(())
1008 * # }
1009 * ```
1010 */
1011 pub fn file_size(&self) -> Option<u64> {
1012 self.file_size
1013 }
1014
1015 /**
1016 * Returns the `HOMEPAGE` value if set. This is an optional field.
1017 *
1018 * ## Example
1019 *
1020 * Parse [`pkg_summary.gz`] and return `HOMEPAGE` for `mktool`.
1021 *
1022 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1023 *
1024 * ```
1025 * use flate2::read::GzDecoder;
1026 * use pkgsrc::summary::Summary;
1027 * use std::fs::File;
1028 * use std::io::BufReader;
1029 *
1030 * # fn main() -> std::io::Result<()> {
1031 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1032 * let file = File::open(path)?;
1033 * let decoder = GzDecoder::new(file);
1034 * let reader = BufReader::new(decoder);
1035 *
1036 * let pkgs: Vec<_> = Summary::from_reader(reader)
1037 * .filter_map(Result::ok)
1038 * .collect();
1039 *
1040 * assert_eq!(
1041 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1042 * .expect("mktool not found")
1043 * .homepage(),
1044 * Some("https://github.com/jperkin/mktool/")
1045 * );
1046 *
1047 * # Ok(())
1048 * # }
1049 * ```
1050 */
1051 pub fn homepage(&self) -> Option<&str> {
1052 self.homepage.as_deref()
1053 }
1054
1055 /**
1056 * Returns the `LICENSE` value if set. This is an optional field.
1057 *
1058 * ## Example
1059 *
1060 * Parse [`pkg_summary.gz`] and return `LICENSE` for `mktool`.
1061 *
1062 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1063 *
1064 * ```
1065 * use flate2::read::GzDecoder;
1066 * use pkgsrc::summary::Summary;
1067 * use std::fs::File;
1068 * use std::io::BufReader;
1069 *
1070 * # fn main() -> std::io::Result<()> {
1071 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1072 * let file = File::open(path)?;
1073 * let decoder = GzDecoder::new(file);
1074 * let reader = BufReader::new(decoder);
1075 *
1076 * let pkgs: Vec<_> = Summary::from_reader(reader)
1077 * .filter_map(Result::ok)
1078 * .collect();
1079 *
1080 * assert_eq!(
1081 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1082 * .expect("mktool not found")
1083 * .license(),
1084 * Some("isc")
1085 * );
1086 *
1087 * # Ok(())
1088 * # }
1089 * ```
1090 */
1091 pub fn license(&self) -> Option<&str> {
1092 self.license.as_deref()
1093 }
1094
1095 /**
1096 * Returns the `MACHINE_ARCH` value. This is a required field.
1097 *
1098 * ## Example
1099 *
1100 * Parse [`pkg_summary.gz`] and return `MACHINE_ARCH` for `mktool`.
1101 *
1102 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1103 *
1104 * ```
1105 * use flate2::read::GzDecoder;
1106 * use pkgsrc::summary::Summary;
1107 * use std::fs::File;
1108 * use std::io::BufReader;
1109 *
1110 * # fn main() -> std::io::Result<()> {
1111 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1112 * let file = File::open(path)?;
1113 * let decoder = GzDecoder::new(file);
1114 * let reader = BufReader::new(decoder);
1115 *
1116 * let pkgs: Vec<_> = Summary::from_reader(reader)
1117 * .filter_map(Result::ok)
1118 * .collect();
1119 *
1120 * assert_eq!(
1121 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1122 * .expect("mktool not found")
1123 * .machine_arch(),
1124 * "aarch64"
1125 * );
1126 *
1127 * # Ok(())
1128 * # }
1129 * ```
1130 */
1131 pub fn machine_arch(&self) -> &str {
1132 &self.machine_arch
1133 }
1134
1135 /**
1136 * Returns the `OPSYS` value. This is a required field.
1137 *
1138 * ## Example
1139 *
1140 * Parse [`pkg_summary.gz`] and return `OPSYS` for `mktool`.
1141 *
1142 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1143 *
1144 * ```
1145 * use flate2::read::GzDecoder;
1146 * use pkgsrc::summary::Summary;
1147 * use std::fs::File;
1148 * use std::io::BufReader;
1149 *
1150 * # fn main() -> std::io::Result<()> {
1151 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1152 * let file = File::open(path)?;
1153 * let decoder = GzDecoder::new(file);
1154 * let reader = BufReader::new(decoder);
1155 *
1156 * let pkgs: Vec<_> = Summary::from_reader(reader)
1157 * .filter_map(Result::ok)
1158 * .collect();
1159 *
1160 * assert_eq!(
1161 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1162 * .expect("mktool not found")
1163 * .opsys(),
1164 * "Darwin"
1165 * );
1166 *
1167 * # Ok(())
1168 * # }
1169 * ```
1170 */
1171 pub fn opsys(&self) -> &str {
1172 &self.opsys
1173 }
1174
1175 /**
1176 * Returns the `OS_VERSION` value. This is a required field.
1177 *
1178 * ## Example
1179 *
1180 * Parse [`pkg_summary.gz`] and return `OS_VERSION` for `mktool`.
1181 *
1182 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1183 *
1184 * ```
1185 * use flate2::read::GzDecoder;
1186 * use pkgsrc::summary::Summary;
1187 * use std::fs::File;
1188 * use std::io::BufReader;
1189 *
1190 * # fn main() -> std::io::Result<()> {
1191 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1192 * let file = File::open(path)?;
1193 * let decoder = GzDecoder::new(file);
1194 * let reader = BufReader::new(decoder);
1195 *
1196 * let pkgs: Vec<_> = Summary::from_reader(reader)
1197 * .filter_map(Result::ok)
1198 * .collect();
1199 *
1200 * assert_eq!(
1201 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1202 * .expect("mktool not found")
1203 * .os_version(),
1204 * "23.6.0"
1205 * );
1206 *
1207 * # Ok(())
1208 * # }
1209 * ```
1210 */
1211 pub fn os_version(&self) -> &str {
1212 &self.os_version
1213 }
1214
1215 /**
1216 * Returns the `PKG_OPTIONS` value if set. This is an optional field.
1217 *
1218 * ## Example
1219 *
1220 * Parse [`pkg_summary.gz`] and return `PKG_OPTIONS` for `mktool` (none)
1221 * and `freeglut` (some).
1222 *
1223 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1224 *
1225 * ```
1226 * use flate2::read::GzDecoder;
1227 * use pkgsrc::summary::Summary;
1228 * use std::fs::File;
1229 * use std::io::BufReader;
1230 *
1231 * # fn main() -> std::io::Result<()> {
1232 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1233 * let file = File::open(path)?;
1234 * let decoder = GzDecoder::new(file);
1235 * let reader = BufReader::new(decoder);
1236 *
1237 * let pkgs: Vec<_> = Summary::from_reader(reader)
1238 * .filter_map(Result::ok)
1239 * .collect();
1240 *
1241 * assert_eq!(
1242 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1243 * .expect("mktool not found")
1244 * .pkg_options(),
1245 * None
1246 * );
1247 *
1248 * // freeglut has PKG_OPTIONS.
1249 * assert_eq!(
1250 * pkgs.iter().find(|p| p.pkgname() == "freeglut-3.6.0")
1251 * .expect("freeglut not found")
1252 * .pkg_options(),
1253 * Some("x11")
1254 * );
1255 *
1256 * # Ok(())
1257 * # }
1258 * ```
1259 */
1260 pub fn pkg_options(&self) -> Option<&str> {
1261 self.pkg_options.as_deref()
1262 }
1263
1264 /**
1265 * Returns the `PKGNAME` value. This is a required field.
1266 *
1267 * ## Example
1268 *
1269 * Parse [`pkg_summary.gz`] and return `PKGNAME` for `mktool`.
1270 *
1271 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1272 *
1273 * ```
1274 * use flate2::read::GzDecoder;
1275 * use pkgsrc::summary::Summary;
1276 * use std::fs::File;
1277 * use std::io::BufReader;
1278 *
1279 * # fn main() -> std::io::Result<()> {
1280 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1281 * let file = File::open(path)?;
1282 * let decoder = GzDecoder::new(file);
1283 * let reader = BufReader::new(decoder);
1284 *
1285 * let pkgs: Vec<_> = Summary::from_reader(reader)
1286 * .filter_map(Result::ok)
1287 * .collect();
1288 *
1289 * // Find the mktool package
1290 * let mktool = pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2");
1291 * assert!(mktool.is_some());
1292 *
1293 * // Using the PkgName API we can also access just the base or version
1294 * assert_eq!(mktool.unwrap().pkgname().pkgbase(), "mktool");
1295 * assert_eq!(mktool.unwrap().pkgname().pkgversion(), "1.4.2");
1296 *
1297 * # Ok(())
1298 * # }
1299 * ```
1300 */
1301 pub fn pkgname(&self) -> &PkgName {
1302 &self.pkgname
1303 }
1304
1305 /**
1306 * Returns the `PKGPATH` value. This is a required field.
1307 *
1308 * ## Example
1309 *
1310 * Parse [`pkg_summary.gz`] and return `PKGPATH` for `mktool`.
1311 *
1312 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1313 *
1314 * ```
1315 * use flate2::read::GzDecoder;
1316 * use pkgsrc::summary::Summary;
1317 * use std::fs::File;
1318 * use std::io::BufReader;
1319 *
1320 * # fn main() -> std::io::Result<()> {
1321 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1322 * let file = File::open(path)?;
1323 * let decoder = GzDecoder::new(file);
1324 * let reader = BufReader::new(decoder);
1325 *
1326 * let pkgs: Vec<_> = Summary::from_reader(reader)
1327 * .filter_map(Result::ok)
1328 * .collect();
1329 *
1330 * assert_eq!(
1331 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1332 * .expect("mktool not found")
1333 * .pkgpath(),
1334 * "pkgtools/mktool"
1335 * );
1336 *
1337 * # Ok(())
1338 * # }
1339 * ```
1340 */
1341 pub fn pkgpath(&self) -> &str {
1342 &self.pkgpath
1343 }
1344
1345 /**
1346 * Returns the `PKGTOOLS_VERSION` value. This is a required field.
1347 *
1348 * ## Example
1349 *
1350 * Parse [`pkg_summary.gz`] and return `PKGTOOLS_VERSION` for `mktool`.
1351 *
1352 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1353 *
1354 * ```
1355 * use flate2::read::GzDecoder;
1356 * use pkgsrc::summary::Summary;
1357 * use std::fs::File;
1358 * use std::io::BufReader;
1359 *
1360 * # fn main() -> std::io::Result<()> {
1361 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1362 * let file = File::open(path)?;
1363 * let decoder = GzDecoder::new(file);
1364 * let reader = BufReader::new(decoder);
1365 *
1366 * let pkgs: Vec<_> = Summary::from_reader(reader)
1367 * .filter_map(Result::ok)
1368 * .collect();
1369 *
1370 * assert_eq!(
1371 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1372 * .expect("mktool not found")
1373 * .pkgtools_version(),
1374 * "20091115"
1375 * );
1376 *
1377 * # Ok(())
1378 * # }
1379 * ```
1380 */
1381 pub fn pkgtools_version(&self) -> &str {
1382 &self.pkgtools_version
1383 }
1384
1385 /**
1386 * Returns the `PREV_PKGPATH` value if set. This is an optional field.
1387 *
1388 * ## Example
1389 *
1390 * Parse [`pkg_summary.gz`] and return `PREV_PKGPATH` for `mktool` (none)
1391 * and `ansible` (some).
1392 *
1393 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1394 *
1395 * ```
1396 * use flate2::read::GzDecoder;
1397 * use pkgsrc::summary::Summary;
1398 * use std::fs::File;
1399 * use std::io::BufReader;
1400 *
1401 * # fn main() -> std::io::Result<()> {
1402 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1403 * let file = File::open(path)?;
1404 * let decoder = GzDecoder::new(file);
1405 * let reader = BufReader::new(decoder);
1406 *
1407 * let pkgs: Vec<_> = Summary::from_reader(reader)
1408 * .filter_map(Result::ok)
1409 * .collect();
1410 *
1411 * assert_eq!(
1412 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1413 * .expect("mktool not found")
1414 * .prev_pkgpath(),
1415 * None
1416 * );
1417 *
1418 * assert_eq!(
1419 * pkgs.iter().find(|p| p.pkgname() == "ansible-12.2.0")
1420 * .expect("ansible not found")
1421 * .prev_pkgpath(),
1422 * Some("sysutils/ansible2")
1423 * );
1424 *
1425 * # Ok(())
1426 * # }
1427 * ```
1428 */
1429 pub fn prev_pkgpath(&self) -> Option<&str> {
1430 self.prev_pkgpath.as_deref()
1431 }
1432
1433 /**
1434 * Returns a [`Vec`] containing optional `PROVIDES` values, or [`None`] if
1435 * there are none.
1436 *
1437 * ## Example
1438 *
1439 * Parse [`pkg_summary.gz`] and return `PROVIDES` for `mktool` (none) and
1440 * `CUnit` (multiple).
1441 *
1442 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1443 *
1444 * ```
1445 * use flate2::read::GzDecoder;
1446 * use pkgsrc::summary::Summary;
1447 * use std::fs::File;
1448 * use std::io::BufReader;
1449 *
1450 * # fn main() -> std::io::Result<()> {
1451 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1452 * let file = File::open(path)?;
1453 * let decoder = GzDecoder::new(file);
1454 * let reader = BufReader::new(decoder);
1455 *
1456 * let pkgs: Vec<_> = Summary::from_reader(reader)
1457 * .filter_map(Result::ok)
1458 * .collect();
1459 *
1460 * assert_eq!(
1461 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1462 * .expect("mktool not found")
1463 * .provides(),
1464 * None
1465 * );
1466 *
1467 * // CUnit provides 2 shared libraries
1468 * assert_eq!(
1469 * pkgs.iter().find(|p| p.pkgname() == "CUnit-2.1.3nb1")
1470 * .expect("CUnit not found")
1471 * .provides()
1472 * .map(|v| v.len()),
1473 * Some(2)
1474 * );
1475 *
1476 * # Ok(())
1477 * # }
1478 * ```
1479 */
1480 pub fn provides(&self) -> Option<&[String]> {
1481 self.provides.as_deref()
1482 }
1483
1484 /**
1485 * Returns a [`Vec`] containing optional `REQUIRES` values, or [`None`] if
1486 * there are none.
1487 *
1488 * ## Example
1489 *
1490 * Parse [`pkg_summary.gz`] and return `REQUIRES` for `mktool` (none) and
1491 * `SDL_image` (multiple).
1492 *
1493 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1494 *
1495 * ```
1496 * use flate2::read::GzDecoder;
1497 * use pkgsrc::summary::Summary;
1498 * use std::fs::File;
1499 * use std::io::BufReader;
1500 *
1501 * # fn main() -> std::io::Result<()> {
1502 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1503 * let file = File::open(path)?;
1504 * let decoder = GzDecoder::new(file);
1505 * let reader = BufReader::new(decoder);
1506 *
1507 * let pkgs: Vec<_> = Summary::from_reader(reader)
1508 * .filter_map(Result::ok)
1509 * .collect();
1510 *
1511 * assert_eq!(
1512 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1513 * .expect("mktool not found")
1514 * .requires(),
1515 * None
1516 * );
1517 *
1518 * // SDL_image has 3 REQUIRES entries
1519 * assert_eq!(
1520 * pkgs.iter().find(|p| p.pkgname() == "SDL_image-1.2.12nb16")
1521 * .expect("SDL_image not found")
1522 * .requires()
1523 * .map(|v| v.len()),
1524 * Some(3)
1525 * );
1526 *
1527 * # Ok(())
1528 * # }
1529 * ```
1530 */
1531 pub fn requires(&self) -> Option<&[String]> {
1532 self.requires.as_deref()
1533 }
1534
1535 /**
1536 * Returns the `SIZE_PKG` value. This is a required field.
1537 *
1538 * ## Example
1539 *
1540 * Parse [`pkg_summary.gz`] and return `SIZE_PKG` for `mktool`.
1541 *
1542 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1543 *
1544 * ```
1545 * use flate2::read::GzDecoder;
1546 * use pkgsrc::summary::Summary;
1547 * use std::fs::File;
1548 * use std::io::BufReader;
1549 *
1550 * # fn main() -> std::io::Result<()> {
1551 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1552 * let file = File::open(path)?;
1553 * let decoder = GzDecoder::new(file);
1554 * let reader = BufReader::new(decoder);
1555 *
1556 * let pkgs: Vec<_> = Summary::from_reader(reader)
1557 * .filter_map(Result::ok)
1558 * .collect();
1559 *
1560 * assert_eq!(
1561 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1562 * .expect("mktool not found")
1563 * .size_pkg(),
1564 * 6999600
1565 * );
1566 *
1567 * # Ok(())
1568 * # }
1569 * ```
1570 */
1571 pub fn size_pkg(&self) -> u64 {
1572 self.size_pkg
1573 }
1574
1575 /**
1576 * Returns a [`Vec`] containing optional `SUPERSEDES` values, or [`None`]
1577 * if there are none.
1578 *
1579 * ## Example
1580 *
1581 * Parse [`pkg_summary.gz`] and return `SUPERSEDES` for `mktool` (none) and
1582 * `at-spi2-core` (multiple).
1583 *
1584 * [`pkg_summary.gz`]: https://github.com/jperkin/pkgsrc-rs/blob/master/tests/data/summary/pkg_summary.gz
1585 *
1586 * ```
1587 * use flate2::read::GzDecoder;
1588 * use pkgsrc::summary::Summary;
1589 * use std::fs::File;
1590 * use std::io::BufReader;
1591 *
1592 * # fn main() -> std::io::Result<()> {
1593 * let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/summary/pkg_summary.gz");
1594 * let file = File::open(path)?;
1595 * let decoder = GzDecoder::new(file);
1596 * let reader = BufReader::new(decoder);
1597 *
1598 * let pkgs: Vec<_> = Summary::from_reader(reader)
1599 * .filter_map(Result::ok)
1600 * .collect();
1601 *
1602 * assert_eq!(
1603 * pkgs.iter().find(|p| p.pkgname() == "mktool-1.4.2")
1604 * .expect("mktool not found")
1605 * .supersedes(),
1606 * None
1607 * );
1608 *
1609 * assert_eq!(
1610 * pkgs.iter().find(|p| p.pkgname() == "at-spi2-core-2.58.1")
1611 * .expect("at-spi2-core not found")
1612 * .supersedes(),
1613 * Some(["at-spi2-atk-[0-9]*", "atk-[0-9]*"].map(String::from).as_slice())
1614 * );
1615 *
1616 * # Ok(())
1617 * # }
1618 * ```
1619 */
1620 pub fn supersedes(&self) -> Option<&[String]> {
1621 self.supersedes.as_deref()
1622 }
1623}
1624
1625impl FromStr for Summary {
1626 type Err = Error;
1627
1628 fn from_str(s: &str) -> Result<Self> {
1629 Summary::parse(s).map_err(Error::from)
1630 }
1631}
1632
1633fn parse_summary(
1634 s: &str,
1635 allow_unknown: bool,
1636 allow_incomplete: bool,
1637) -> Result<Summary> {
1638 // For allow_unknown/allow_incomplete, we need to wrap the parsing
1639 if allow_unknown || allow_incomplete {
1640 parse_summary_lenient(s, allow_unknown, allow_incomplete)
1641 } else {
1642 Summary::parse(s).map_err(Error::from)
1643 }
1644}
1645
1646fn parse_summary_lenient(
1647 s: &str,
1648 allow_unknown: bool,
1649 allow_incomplete: bool,
1650) -> Result<Summary> {
1651 use crate::kv::FromKv;
1652
1653 // State for each field
1654 let mut build_date: Option<String> = None;
1655 let mut categories: Option<Vec<String>> = None;
1656 let mut comment: Option<String> = None;
1657 let mut conflicts: Option<Vec<String>> = None;
1658 let mut depends: Option<Vec<String>> = None;
1659 let mut description: Option<Vec<String>> = None;
1660 let mut file_cksum: Option<String> = None;
1661 let mut file_name: Option<String> = None;
1662 let mut file_size: Option<u64> = None;
1663 let mut homepage: Option<String> = None;
1664 let mut license: Option<String> = None;
1665 let mut machine_arch: Option<String> = None;
1666 let mut opsys: Option<String> = None;
1667 let mut os_version: Option<String> = None;
1668 let mut pkgname: Option<PkgName> = None;
1669 let mut pkgpath: Option<String> = None;
1670 let mut pkgtools_version: Option<String> = None;
1671 let mut pkg_options: Option<String> = None;
1672 let mut prev_pkgpath: Option<String> = None;
1673 let mut provides: Option<Vec<String>> = None;
1674 let mut requires: Option<Vec<String>> = None;
1675 let mut size_pkg: Option<u64> = None;
1676 let mut supersedes: Option<Vec<String>> = None;
1677
1678 for line in s.lines() {
1679 if line.is_empty() {
1680 continue;
1681 }
1682
1683 let line_offset = line.as_ptr() as usize - s.as_ptr() as usize;
1684
1685 let (key, value) =
1686 line.split_once('=').ok_or_else(|| Error::ParseLine {
1687 context: ErrorContext::new(Span {
1688 offset: line_offset,
1689 len: line.len(),
1690 }),
1691 })?;
1692
1693 let value_offset = line_offset + key.len() + 1;
1694 let value_span = Span {
1695 offset: value_offset,
1696 len: value.len(),
1697 };
1698
1699 match key {
1700 "BUILD_DATE" => {
1701 build_date = Some(
1702 <String as FromKv>::from_kv(value, value_span)
1703 .map_err(kv_to_summary_error)?,
1704 );
1705 }
1706 "CATEGORIES" => {
1707 let items: Vec<String> =
1708 value.split_whitespace().map(String::from).collect();
1709 categories = Some(items);
1710 }
1711 "COMMENT" => {
1712 comment = Some(
1713 <String as FromKv>::from_kv(value, value_span)
1714 .map_err(kv_to_summary_error)?,
1715 );
1716 }
1717 "CONFLICTS" => {
1718 let mut vec = conflicts.unwrap_or_default();
1719 vec.push(
1720 <String as FromKv>::from_kv(value, value_span)
1721 .map_err(kv_to_summary_error)?,
1722 );
1723 conflicts = Some(vec);
1724 }
1725 "DEPENDS" => {
1726 let mut vec = depends.unwrap_or_default();
1727 vec.push(
1728 <String as FromKv>::from_kv(value, value_span)
1729 .map_err(kv_to_summary_error)?,
1730 );
1731 depends = Some(vec);
1732 }
1733 "DESCRIPTION" => {
1734 let mut vec = description.unwrap_or_default();
1735 vec.push(
1736 <String as FromKv>::from_kv(value, value_span)
1737 .map_err(kv_to_summary_error)?,
1738 );
1739 description = Some(vec);
1740 }
1741 "FILE_CKSUM" => {
1742 file_cksum = Some(
1743 <String as FromKv>::from_kv(value, value_span)
1744 .map_err(kv_to_summary_error)?,
1745 );
1746 }
1747 "FILE_NAME" => {
1748 file_name = Some(
1749 <String as FromKv>::from_kv(value, value_span)
1750 .map_err(kv_to_summary_error)?,
1751 );
1752 }
1753 "FILE_SIZE" => {
1754 file_size = Some(
1755 <u64 as FromKv>::from_kv(value, value_span)
1756 .map_err(kv_to_summary_error)?,
1757 );
1758 }
1759 "HOMEPAGE" => {
1760 homepage = Some(
1761 <String as FromKv>::from_kv(value, value_span)
1762 .map_err(kv_to_summary_error)?,
1763 );
1764 }
1765 "LICENSE" => {
1766 license = Some(
1767 <String as FromKv>::from_kv(value, value_span)
1768 .map_err(kv_to_summary_error)?,
1769 );
1770 }
1771 "MACHINE_ARCH" => {
1772 machine_arch = Some(
1773 <String as FromKv>::from_kv(value, value_span)
1774 .map_err(kv_to_summary_error)?,
1775 );
1776 }
1777 "OPSYS" => {
1778 opsys = Some(
1779 <String as FromKv>::from_kv(value, value_span)
1780 .map_err(kv_to_summary_error)?,
1781 );
1782 }
1783 "OS_VERSION" => {
1784 os_version = Some(
1785 <String as FromKv>::from_kv(value, value_span)
1786 .map_err(kv_to_summary_error)?,
1787 );
1788 }
1789 "PKGNAME" => {
1790 pkgname = Some(
1791 <PkgName as FromKv>::from_kv(value, value_span)
1792 .map_err(kv_to_summary_error)?,
1793 );
1794 }
1795 "PKGPATH" => {
1796 pkgpath = Some(
1797 <String as FromKv>::from_kv(value, value_span)
1798 .map_err(kv_to_summary_error)?,
1799 );
1800 }
1801 "PKGTOOLS_VERSION" => {
1802 pkgtools_version = Some(
1803 <String as FromKv>::from_kv(value, value_span)
1804 .map_err(kv_to_summary_error)?,
1805 );
1806 }
1807 "PKG_OPTIONS" => {
1808 pkg_options = Some(
1809 <String as FromKv>::from_kv(value, value_span)
1810 .map_err(kv_to_summary_error)?,
1811 );
1812 }
1813 "PREV_PKGPATH" => {
1814 prev_pkgpath = Some(
1815 <String as FromKv>::from_kv(value, value_span)
1816 .map_err(kv_to_summary_error)?,
1817 );
1818 }
1819 "PROVIDES" => {
1820 let mut vec = provides.unwrap_or_default();
1821 vec.push(
1822 <String as FromKv>::from_kv(value, value_span)
1823 .map_err(kv_to_summary_error)?,
1824 );
1825 provides = Some(vec);
1826 }
1827 "REQUIRES" => {
1828 let mut vec = requires.unwrap_or_default();
1829 vec.push(
1830 <String as FromKv>::from_kv(value, value_span)
1831 .map_err(kv_to_summary_error)?,
1832 );
1833 requires = Some(vec);
1834 }
1835 "SIZE_PKG" => {
1836 size_pkg = Some(
1837 <u64 as FromKv>::from_kv(value, value_span)
1838 .map_err(kv_to_summary_error)?,
1839 );
1840 }
1841 "SUPERSEDES" => {
1842 let mut vec = supersedes.unwrap_or_default();
1843 vec.push(
1844 <String as FromKv>::from_kv(value, value_span)
1845 .map_err(kv_to_summary_error)?,
1846 );
1847 supersedes = Some(vec);
1848 }
1849 unknown => {
1850 if !allow_unknown {
1851 return Err(Error::UnknownVariable {
1852 variable: unknown.to_string(),
1853 context: ErrorContext::new(Span {
1854 offset: line_offset,
1855 len: key.len(),
1856 }),
1857 });
1858 }
1859 }
1860 }
1861 }
1862
1863 // Extract values, using defaults for missing required fields if allow_incomplete
1864 let build_date = if allow_incomplete {
1865 build_date.unwrap_or_default()
1866 } else {
1867 build_date.ok_or_else(|| Error::Incomplete {
1868 field: "BUILD_DATE".to_string(),
1869 context: ErrorContext::default(),
1870 })?
1871 };
1872
1873 let categories = if allow_incomplete {
1874 categories.unwrap_or_default()
1875 } else {
1876 categories.ok_or_else(|| Error::Incomplete {
1877 field: "CATEGORIES".to_string(),
1878 context: ErrorContext::default(),
1879 })?
1880 };
1881
1882 let comment = if allow_incomplete {
1883 comment.unwrap_or_default()
1884 } else {
1885 comment.ok_or_else(|| Error::Incomplete {
1886 field: "COMMENT".to_string(),
1887 context: ErrorContext::default(),
1888 })?
1889 };
1890
1891 let description = if allow_incomplete {
1892 description.unwrap_or_default()
1893 } else {
1894 description.ok_or_else(|| Error::Incomplete {
1895 field: "DESCRIPTION".to_string(),
1896 context: ErrorContext::default(),
1897 })?
1898 };
1899
1900 let machine_arch = if allow_incomplete {
1901 machine_arch.unwrap_or_default()
1902 } else {
1903 machine_arch.ok_or_else(|| Error::Incomplete {
1904 field: "MACHINE_ARCH".to_string(),
1905 context: ErrorContext::default(),
1906 })?
1907 };
1908
1909 let opsys = if allow_incomplete {
1910 opsys.unwrap_or_default()
1911 } else {
1912 opsys.ok_or_else(|| Error::Incomplete {
1913 field: "OPSYS".to_string(),
1914 context: ErrorContext::default(),
1915 })?
1916 };
1917
1918 let os_version = if allow_incomplete {
1919 os_version.unwrap_or_default()
1920 } else {
1921 os_version.ok_or_else(|| Error::Incomplete {
1922 field: "OS_VERSION".to_string(),
1923 context: ErrorContext::default(),
1924 })?
1925 };
1926
1927 let pkgname = if allow_incomplete {
1928 pkgname.unwrap_or_else(|| PkgName::new("unknown-0"))
1929 } else {
1930 pkgname.ok_or_else(|| Error::Incomplete {
1931 field: "PKGNAME".to_string(),
1932 context: ErrorContext::default(),
1933 })?
1934 };
1935
1936 let pkgpath = if allow_incomplete {
1937 pkgpath.unwrap_or_default()
1938 } else {
1939 pkgpath.ok_or_else(|| Error::Incomplete {
1940 field: "PKGPATH".to_string(),
1941 context: ErrorContext::default(),
1942 })?
1943 };
1944
1945 let pkgtools_version = if allow_incomplete {
1946 pkgtools_version.unwrap_or_default()
1947 } else {
1948 pkgtools_version.ok_or_else(|| Error::Incomplete {
1949 field: "PKGTOOLS_VERSION".to_string(),
1950 context: ErrorContext::default(),
1951 })?
1952 };
1953
1954 let size_pkg = if allow_incomplete {
1955 size_pkg.unwrap_or(0)
1956 } else {
1957 size_pkg.ok_or_else(|| Error::Incomplete {
1958 field: "SIZE_PKG".to_string(),
1959 context: ErrorContext::default(),
1960 })?
1961 };
1962
1963 Ok(Summary {
1964 build_date,
1965 categories,
1966 comment,
1967 conflicts,
1968 depends,
1969 description,
1970 file_cksum,
1971 file_name,
1972 file_size,
1973 homepage,
1974 license,
1975 machine_arch,
1976 opsys,
1977 os_version,
1978 pkgname,
1979 pkgpath,
1980 pkgtools_version,
1981 pkg_options,
1982 prev_pkgpath,
1983 provides,
1984 requires,
1985 size_pkg,
1986 supersedes,
1987 })
1988}
1989
1990fn kv_to_summary_error(e: crate::kv::Error) -> Error {
1991 Error::from(e)
1992}
1993
1994/**
1995 * Iterator that parses Summary entries from a [`BufRead`] source.
1996 *
1997 * Created by [`Summary::from_reader`].
1998 */
1999pub struct SummaryIter<R: BufRead> {
2000 reader: R,
2001 line_buf: String,
2002 buffer: String,
2003 record_number: usize,
2004 byte_offset: usize,
2005 entry_start: usize,
2006 allow_unknown: bool,
2007 allow_incomplete: bool,
2008}
2009
2010impl<R: BufRead> Iterator for SummaryIter<R> {
2011 type Item = Result<Summary>;
2012
2013 fn next(&mut self) -> Option<Self::Item> {
2014 self.buffer.clear();
2015 self.entry_start = self.byte_offset;
2016
2017 loop {
2018 self.line_buf.clear();
2019 match self.reader.read_line(&mut self.line_buf) {
2020 Ok(0) => {
2021 return if self.buffer.is_empty() {
2022 None
2023 } else {
2024 let entry = self.record_number;
2025 let entry_start = self.entry_start;
2026 let entry_len = self.buffer.len();
2027 self.record_number += 1;
2028 Some(
2029 parse_summary(
2030 &self.buffer,
2031 self.allow_unknown,
2032 self.allow_incomplete,
2033 )
2034 .map_err(|e: Error| {
2035 e.with_entry_span(Span {
2036 offset: 0,
2037 len: entry_len,
2038 })
2039 .with_entry(entry)
2040 .adjust_offset(entry_start)
2041 }),
2042 )
2043 };
2044 }
2045 Ok(line_bytes) => {
2046 let is_blank =
2047 self.line_buf.trim_end_matches(['\r', '\n']).is_empty();
2048 if is_blank {
2049 self.byte_offset += line_bytes;
2050 if !self.buffer.is_empty() {
2051 let entry = self.record_number;
2052 let entry_start = self.entry_start;
2053 // Trim trailing newline for parsing (doesn't affect offsets
2054 // since FromStr handles any line ending style)
2055 let to_parse =
2056 self.buffer.trim_end_matches(['\r', '\n']);
2057 let entry_len = to_parse.len();
2058 self.record_number += 1;
2059 self.entry_start = self.byte_offset;
2060 return Some(
2061 parse_summary(
2062 to_parse,
2063 self.allow_unknown,
2064 self.allow_incomplete,
2065 )
2066 .map_err(
2067 |e: Error| {
2068 e.with_entry_span(Span {
2069 offset: 0,
2070 len: entry_len,
2071 })
2072 .with_entry(entry)
2073 .adjust_offset(entry_start)
2074 },
2075 ),
2076 );
2077 }
2078 } else {
2079 self.buffer.push_str(&self.line_buf);
2080 self.byte_offset += line_bytes;
2081 }
2082 }
2083 Err(e) => return Some(Err(Error::Io(e))),
2084 }
2085 }
2086 }
2087}
2088
2089impl<R: BufRead> SummaryIter<R> {
2090 /// Allow unknown variables instead of returning an error.
2091 #[must_use]
2092 pub fn allow_unknown(mut self, yes: bool) -> Self {
2093 self.allow_unknown = yes;
2094 self
2095 }
2096
2097 /// Allow incomplete entries missing required fields.
2098 #[must_use]
2099 pub fn allow_incomplete(mut self, yes: bool) -> Self {
2100 self.allow_incomplete = yes;
2101 self
2102 }
2103}
2104
2105/**
2106 * Error type for [`pkg_summary(5)`] parsing operations.
2107 *
2108 * Each error variant includes an [`ErrorContext`] with span information that
2109 * can be used with error reporting libraries like [`ariadne`] or [`miette`].
2110 *
2111 * [`ariadne`]: https://docs.rs/ariadne
2112 * [`miette`]: https://docs.rs/miette
2113 * [`pkg_summary(5)`]: https://man.netbsd.org/pkg_summary.5
2114 */
2115#[derive(Debug, thiserror::Error)]
2116#[non_exhaustive]
2117pub enum Error {
2118 /// The summary is incomplete due to a missing required field.
2119 #[error("missing required field '{field}'")]
2120 Incomplete {
2121 /// The name of the missing field.
2122 field: String,
2123 /// Location context for this error.
2124 context: ErrorContext,
2125 },
2126
2127 /// An underlying I/O error.
2128 #[error(transparent)]
2129 Io(#[from] io::Error),
2130
2131 /// The supplied line is not in the correct `VARIABLE=VALUE` format.
2132 #[error("line is not in VARIABLE=VALUE format")]
2133 ParseLine {
2134 /// Location context for this error.
2135 context: ErrorContext,
2136 },
2137
2138 /// The supplied variable is not a valid [`pkg_summary(5)`] variable.
2139 ///
2140 /// [`pkg_summary(5)`]: https://man.netbsd.org/pkg_summary.5
2141 #[error("'{variable}' is not a valid pkg_summary variable")]
2142 UnknownVariable {
2143 /// The unknown variable name.
2144 variable: String,
2145 /// Location context for this error.
2146 context: ErrorContext,
2147 },
2148
2149 /// Parsing a supplied value as an Integer type failed.
2150 #[error("failed to parse integer")]
2151 ParseInt {
2152 /// The underlying parse error.
2153 #[source]
2154 source: ParseIntError,
2155 /// Location context for this error.
2156 context: ErrorContext,
2157 },
2158
2159 /// A duplicate value was found for a single-value field.
2160 #[error("duplicate value for '{variable}'")]
2161 Duplicate {
2162 /// The name of the duplicated variable.
2163 variable: String,
2164 /// Location context for this error.
2165 context: ErrorContext,
2166 },
2167
2168 /// A generic parse error from the kv module.
2169 #[error("{message}")]
2170 Parse {
2171 /// The error message.
2172 message: String,
2173 /// Location context for this error.
2174 context: ErrorContext,
2175 },
2176}
2177
2178impl From<crate::kv::Error> for Error {
2179 fn from(e: crate::kv::Error) -> Self {
2180 match e {
2181 crate::kv::Error::ParseLine(span) => Self::ParseLine {
2182 context: ErrorContext::new(span),
2183 },
2184 crate::kv::Error::Incomplete(field) => Self::Incomplete {
2185 field,
2186 context: ErrorContext::default(),
2187 },
2188 crate::kv::Error::UnknownVariable { variable, span } => {
2189 Self::UnknownVariable {
2190 variable,
2191 context: ErrorContext::new(span),
2192 }
2193 }
2194 crate::kv::Error::ParseInt { source, span } => Self::ParseInt {
2195 source,
2196 context: ErrorContext::new(span),
2197 },
2198 crate::kv::Error::Parse { message, span } => Self::Parse {
2199 message,
2200 context: ErrorContext::new(span),
2201 },
2202 }
2203 }
2204}
2205
2206impl Error {
2207 /**
2208 * Returns the entry index where the error occurred.
2209 *
2210 * Only set when parsing multiple entries via [`Summary::from_reader`].
2211 */
2212 pub fn entry(&self) -> Option<usize> {
2213 match self {
2214 Self::Incomplete { context, .. }
2215 | Self::ParseLine { context, .. }
2216 | Self::UnknownVariable { context, .. }
2217 | Self::ParseInt { context, .. }
2218 | Self::Duplicate { context, .. }
2219 | Self::Parse { context, .. } => context.entry(),
2220 Self::Io(_) => None,
2221 }
2222 }
2223
2224 /**
2225 * Returns the span information for this error.
2226 *
2227 * The span contains the byte offset and length of the problematic region.
2228 */
2229 pub fn span(&self) -> Option<Span> {
2230 match self {
2231 Self::Incomplete { context, .. }
2232 | Self::ParseLine { context, .. }
2233 | Self::UnknownVariable { context, .. }
2234 | Self::ParseInt { context, .. }
2235 | Self::Duplicate { context, .. }
2236 | Self::Parse { context, .. } => context.span(),
2237 Self::Io(_) => None,
2238 }
2239 }
2240
2241 fn with_entry(self, entry: usize) -> Self {
2242 match self {
2243 Self::Incomplete { field, context } => Self::Incomplete {
2244 field,
2245 context: context.with_entry(entry),
2246 },
2247 Self::ParseLine { context } => Self::ParseLine {
2248 context: context.with_entry(entry),
2249 },
2250 Self::UnknownVariable { variable, context } => {
2251 Self::UnknownVariable {
2252 variable,
2253 context: context.with_entry(entry),
2254 }
2255 }
2256 Self::ParseInt { source, context } => Self::ParseInt {
2257 source,
2258 context: context.with_entry(entry),
2259 },
2260 Self::Duplicate { variable, context } => Self::Duplicate {
2261 variable,
2262 context: context.with_entry(entry),
2263 },
2264 Self::Parse { message, context } => Self::Parse {
2265 message,
2266 context: context.with_entry(entry),
2267 },
2268 Self::Io(e) => Self::Io(e),
2269 }
2270 }
2271
2272 fn adjust_offset(self, base: usize) -> Self {
2273 match self {
2274 Self::Incomplete { field, context } => Self::Incomplete {
2275 field,
2276 context: context.adjust_offset(base),
2277 },
2278 Self::ParseLine { context } => Self::ParseLine {
2279 context: context.adjust_offset(base),
2280 },
2281 Self::UnknownVariable { variable, context } => {
2282 Self::UnknownVariable {
2283 variable,
2284 context: context.adjust_offset(base),
2285 }
2286 }
2287 Self::ParseInt { source, context } => Self::ParseInt {
2288 source,
2289 context: context.adjust_offset(base),
2290 },
2291 Self::Duplicate { variable, context } => Self::Duplicate {
2292 variable,
2293 context: context.adjust_offset(base),
2294 },
2295 Self::Parse { message, context } => Self::Parse {
2296 message,
2297 context: context.adjust_offset(base),
2298 },
2299 Self::Io(e) => Self::Io(e),
2300 }
2301 }
2302
2303 fn with_entry_span(self, span: Span) -> Self {
2304 match self {
2305 Self::Incomplete { field, context } => Self::Incomplete {
2306 field,
2307 context: context.with_span_if_none(span),
2308 },
2309 other => other,
2310 }
2311 }
2312}
2313
2314#[cfg(test)]
2315mod tests {
2316 use super::*;
2317
2318 #[test]
2319 fn test_err() {
2320 let err = Summary::from_str("BUILD_DATE").unwrap_err();
2321 assert!(matches!(err, Error::ParseLine { .. }));
2322
2323 let err = Summary::from_str("BILD_DATE=").unwrap_err();
2324 assert!(matches!(err, Error::UnknownVariable { .. }));
2325
2326 // FILE_SIZE=NaN with all required fields should error on parse
2327 let input = indoc! {"
2328 BUILD_DATE=2019-08-12
2329 CATEGORIES=devel
2330 COMMENT=test
2331 DESCRIPTION=test
2332 MACHINE_ARCH=x86_64
2333 OPSYS=NetBSD
2334 OS_VERSION=9.0
2335 PKGNAME=test-1.0
2336 PKGPATH=devel/test
2337 PKGTOOLS_VERSION=20091115
2338 SIZE_PKG=1234
2339 FILE_SIZE=NaN
2340 "};
2341 let err = Summary::from_str(input).unwrap_err();
2342 assert!(matches!(err, Error::ParseInt { .. }));
2343
2344 let err = Summary::from_str("FILE_SIZE=1234").unwrap_err();
2345 assert!(matches!(err, Error::Incomplete { .. }));
2346 }
2347
2348 #[test]
2349 fn test_error_context() {
2350 // Test that errors include span context
2351 let err =
2352 Summary::from_str("BUILD_DATE=2019-08-12\nBAD LINE\n").unwrap_err();
2353 assert!(matches!(err, Error::ParseLine { .. }));
2354 let span = err.span().expect("should have span");
2355 assert_eq!(span.offset, 22); // byte offset to "BAD LINE" (0-based)
2356 assert_eq!(span.len, 8); // length of "BAD LINE"
2357 assert!(err.entry().is_none()); // No entry context when parsing directly
2358
2359 // Test error includes key name for UnknownVariable errors
2360 let err = Summary::from_str("INVALID_KEY=value\n").unwrap_err();
2361 assert!(
2362 matches!(err, Error::UnknownVariable { variable, .. } if variable == "INVALID_KEY")
2363 );
2364
2365 // Test multi-entry parsing includes entry index
2366 let input = indoc! {"
2367 PKGNAME=good-1.0
2368 COMMENT=test
2369 SIZE_PKG=100
2370 BUILD_DATE=2019-08-12
2371 CATEGORIES=test
2372 DESCRIPTION=test
2373 MACHINE_ARCH=x86_64
2374 OPSYS=Darwin
2375 OS_VERSION=18.7.0
2376 PKGPATH=test/good
2377 PKGTOOLS_VERSION=20091115
2378
2379 PKGNAME=bad-1.0
2380 COMMENT=test
2381 SIZE_PKG=100
2382 BUILD_DATEFOO=2019-08-12
2383 CATEGORIES=test
2384 "};
2385 let mut iter = Summary::from_reader(input.trim().as_bytes());
2386
2387 // First entry should parse successfully
2388 let first = iter.next().unwrap();
2389 assert!(first.is_ok());
2390
2391 // Second entry should fail with context
2392 let second = iter.next().unwrap();
2393 assert!(second.is_err());
2394 let err = second.unwrap_err();
2395 assert_eq!(err.entry(), Some(1)); // 0-based entry index
2396 }
2397
2398 #[test]
2399 fn test_lenient_parse_mode() -> Result<()> {
2400 let input = indoc! {"
2401 PKGNAME=testpkg-1.0
2402 UNKNOWN_FIELD=value
2403 COMMENT=Test package
2404 BUILD_DATE=2019-08-12 15:58:02 +0100
2405 CATEGORIES=test
2406 DESCRIPTION=Test description
2407 MACHINE_ARCH=x86_64
2408 OPSYS=Darwin
2409 OS_VERSION=18.7.0
2410 PKGPATH=test/pkg
2411 PKGTOOLS_VERSION=20091115
2412 SIZE_PKG=100
2413 "};
2414 let trimmed = input.trim();
2415
2416 let err = Summary::from_str(trimmed).unwrap_err();
2417 assert!(
2418 matches!(err, Error::UnknownVariable { variable, .. } if variable == "UNKNOWN_FIELD")
2419 );
2420
2421 let pkg = parse_summary(trimmed, true, false)?;
2422 assert_eq!(pkg.pkgname().pkgname(), "testpkg-1.0");
2423
2424 let pkg = SummaryBuilder::new()
2425 .allow_unknown(true)
2426 .vars(trimmed.lines())
2427 .build()?;
2428 assert_eq!(pkg.pkgname().pkgname(), "testpkg-1.0");
2429
2430 Ok(())
2431 }
2432
2433 #[test]
2434 fn test_iter_with_options_allow_unknown() -> Result<()> {
2435 let input = indoc! {"
2436 PKGNAME=iterpkg-1.0
2437 COMMENT=Iterator test
2438 UNKNOWN=value
2439 BUILD_DATE=2019-08-12 15:58:02 +0100
2440 CATEGORIES=test
2441 DESCRIPTION=Iterator description
2442 MACHINE_ARCH=x86_64
2443 OPSYS=Darwin
2444 OS_VERSION=18.7.0
2445 PKGPATH=test/iterpkg
2446 PKGTOOLS_VERSION=20091115
2447 SIZE_PKG=100
2448 "};
2449
2450 // Without allow_unknown should fail
2451 let mut iter = Summary::from_reader(input.trim().as_bytes());
2452 let result = iter.next().unwrap();
2453 assert!(result.is_err());
2454
2455 // With allow_unknown should succeed
2456 let mut iter =
2457 Summary::from_reader(input.trim().as_bytes()).allow_unknown(true);
2458 let result = iter.next().unwrap();
2459 assert!(result.is_ok());
2460 assert_eq!(result.unwrap().pkgname().pkgname(), "iterpkg-1.0");
2461
2462 Ok(())
2463 }
2464
2465 #[test]
2466 fn test_iter_with_options_allow_incomplete() -> Result<()> {
2467 // Incomplete: missing DESCRIPTION and others
2468 let input = indoc! {"
2469 PKGNAME=incomplete-1.0
2470 COMMENT=Incomplete test
2471 "};
2472
2473 // Without allow_incomplete should fail
2474 let mut iter = Summary::from_reader(input.trim().as_bytes());
2475 let result = iter.next().unwrap();
2476 assert!(result.is_err());
2477
2478 // With allow_incomplete should succeed
2479 let mut iter = Summary::from_reader(input.trim().as_bytes())
2480 .allow_incomplete(true);
2481 let result = iter.next().unwrap();
2482 assert!(result.is_ok());
2483 let pkg = result.unwrap();
2484 assert_eq!(pkg.pkgname().pkgname(), "incomplete-1.0");
2485 assert_eq!(pkg.comment(), "Incomplete test");
2486 // Missing fields should have defaults
2487 assert!(pkg.categories().is_empty());
2488 assert!(pkg.description().is_empty());
2489
2490 Ok(())
2491 }
2492
2493 #[test]
2494 fn test_display() -> Result<()> {
2495 let input = indoc! {"
2496 PKGNAME=testpkg-1.0
2497 COMMENT=Test package
2498 BUILD_DATE=2019-08-12 15:58:02 +0100
2499 CATEGORIES=test cat2
2500 DESCRIPTION=Line 1
2501 DESCRIPTION=Line 2
2502 MACHINE_ARCH=x86_64
2503 OPSYS=Darwin
2504 OS_VERSION=18.7.0
2505 PKGPATH=test/pkg
2506 PKGTOOLS_VERSION=20091115
2507 SIZE_PKG=100
2508 DEPENDS=dep1-[0-9]*
2509 DEPENDS=dep2>=1.0
2510 "};
2511
2512 let pkg: Summary = input.trim().parse()?;
2513 let output = pkg.to_string();
2514
2515 // Verify key fields are present in output
2516 assert!(output.contains("PKGNAME=testpkg-1.0"));
2517 assert!(output.contains("COMMENT=Test package"));
2518 assert!(output.contains("CATEGORIES=test cat2"));
2519 assert!(output.contains("DESCRIPTION=Line 1"));
2520 assert!(output.contains("DESCRIPTION=Line 2"));
2521 assert!(output.contains("DEPENDS=dep1-[0-9]*"));
2522 assert!(output.contains("DEPENDS=dep2>=1.0"));
2523
2524 Ok(())
2525 }
2526}