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}