alpm_pkginfo/package_info/
mod.rs

1//! High-level PKGINFO handling.
2
3pub mod v1;
4pub mod v2;
5use std::{
6    fmt::Display,
7    fs::File,
8    path::{Path, PathBuf},
9    str::FromStr,
10};
11
12use alpm_common::{FileFormatSchema, MetadataFile};
13
14use crate::{Error, PackageInfoSchema, PackageInfoV1, PackageInfoV2};
15
16/// A representation of the [PKGINFO] file format.
17///
18/// Tracks all available versions of the file format.
19///
20/// [PKGINFO]: https://alpm.archlinux.page/specifications/PKGINFO.5.html
21#[derive(Clone, Debug, PartialEq, serde::Serialize)]
22#[serde(untagged)]
23pub enum PackageInfo {
24    V1(PackageInfoV1),
25    V2(PackageInfoV2),
26}
27
28impl MetadataFile<PackageInfoSchema> for PackageInfo {
29    type Err = Error;
30
31    /// Creates a [`PackageInfo`] from `file`, optionally validated using a [`PackageInfoSchema`].
32    ///
33    /// Opens the `file` and defers to [`PackageInfo::from_reader_with_schema`].
34    ///
35    /// # Note
36    ///
37    /// To automatically derive the [`PackageInfoSchema`], use [`PackageInfo::from_file`].
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use std::{fs::File, io::Write};
43    ///
44    /// use alpm_common::{FileFormatSchema, MetadataFile};
45    /// use alpm_pkginfo::{PackageInfo, PackageInfoSchema};
46    /// use alpm_types::{SchemaVersion, semver_version::Version};
47    ///
48    /// # fn main() -> testresult::TestResult {
49    /// // Prepare a file with PKGINFO data
50    /// let (file, pkginfo_data) = {
51    ///     let pkginfo_data = r#"pkgname = example
52    /// pkgbase = example
53    /// xdata = pkgtype=pkg
54    /// pkgver = 1:1.0.0-1
55    /// pkgdesc = A project that does something
56    /// url = https://example.org/
57    /// builddate = 1729181726
58    /// packager = John Doe <john@example.org>
59    /// size = 181849963
60    /// arch = any
61    /// "#;
62    ///     let pkginfo_file = tempfile::NamedTempFile::new()?;
63    ///     let mut output = File::create(&pkginfo_file)?;
64    ///     write!(output, "{}", pkginfo_data)?;
65    ///     (pkginfo_file, pkginfo_data)
66    /// };
67    ///
68    /// let pkginfo = PackageInfo::from_file_with_schema(
69    ///     file.path().to_path_buf(),
70    ///     Some(PackageInfoSchema::V2(SchemaVersion::new(Version::new(
71    ///         2, 0, 0,
72    ///     )))),
73    /// )?;
74    /// assert_eq!(pkginfo.to_string(), pkginfo_data);
75    /// # Ok(())
76    /// # }
77    /// ```
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if
82    /// - the `file` cannot be opened for reading,
83    /// - no variant of [`PackageInfo`] can be constructed from the contents of `file`,
84    /// - or `schema` is [`Some`] and the [`PackageInfoSchema`] does not match the contents of
85    ///   `file`.
86    fn from_file_with_schema(
87        file: impl AsRef<Path>,
88        schema: Option<PackageInfoSchema>,
89    ) -> Result<Self, Error> {
90        let file = file.as_ref();
91        Self::from_reader_with_schema(
92            File::open(file).map_err(|source| {
93                Error::IoPathError(PathBuf::from(file), "opening the file for reading", source)
94            })?,
95            schema,
96        )
97    }
98
99    /// Creates a [`PackageInfo`] from a `reader`, optionally validated using a
100    /// [`PackageInfoSchema`].
101    ///
102    /// Reads the `reader` to string and defers to [`PackageInfo::from_str_with_schema`].
103    ///
104    /// # Note
105    ///
106    /// To automatically derive the [`PackageInfoSchema`], use [`PackageInfo::from_reader`].
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use std::{fs::File, io::Write};
112    ///
113    /// use alpm_common::MetadataFile;
114    /// use alpm_pkginfo::{PackageInfo, PackageInfoSchema};
115    /// use alpm_types::{SchemaVersion, semver_version::Version};
116    ///
117    /// # fn main() -> testresult::TestResult {
118    /// // Prepare a reader with PKGINFO data
119    /// let (reader, pkginfo_data) = {
120    ///     let pkginfo_data = r#"pkgname = example
121    /// pkgbase = example
122    /// xdata = pkgtype=pkg
123    /// pkgver = 1:1.0.0-1
124    /// pkgdesc = A project that does something
125    /// url = https://example.org/
126    /// builddate = 1729181726
127    /// packager = John Doe <john@example.org>
128    /// size = 181849963
129    /// arch = any
130    /// "#;
131    ///     let pkginfo_file = tempfile::NamedTempFile::new()?;
132    ///     let mut output = File::create(&pkginfo_file)?;
133    ///     write!(output, "{}", pkginfo_data)?;
134    ///     (File::open(&pkginfo_file.path())?, pkginfo_data)
135    /// };
136    ///
137    /// let pkginfo = PackageInfo::from_reader_with_schema(
138    ///     reader,
139    ///     Some(PackageInfoSchema::V2(SchemaVersion::new(Version::new(
140    ///         2, 0, 0,
141    ///     )))),
142    /// )?;
143    /// assert_eq!(pkginfo.to_string(), pkginfo_data);
144    /// # Ok(())
145    /// # }
146    /// ```
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if
151    /// - the `reader` cannot be read to string,
152    /// - no variant of [`PackageInfo`] can be constructed from the contents of the `reader`,
153    /// - or `schema` is [`Some`] and the [`PackageInfoSchema`] does not match the contents of the
154    ///   `reader`.
155    fn from_reader_with_schema(
156        mut reader: impl std::io::Read,
157        schema: Option<PackageInfoSchema>,
158    ) -> Result<Self, Error> {
159        let mut buf = String::new();
160        reader
161            .read_to_string(&mut buf)
162            .map_err(|source| Error::IoReadError {
163                context: "reading PackageInfo data",
164                source,
165            })?;
166        Self::from_str_with_schema(&buf, schema)
167    }
168
169    /// Creates a [`PackageInfo`] from string slice, optionally validated using a
170    /// [`PackageInfoSchema`].
171    ///
172    /// If `schema` is [`None`] attempts to detect the [`PackageInfoSchema`] from `s`.
173    /// Attempts to create a [`PackageInfo`] variant that corresponds to the [`PackageInfoSchema`].
174    ///
175    /// # Note
176    ///
177    /// To automatically derive the [`PackageInfoSchema`], use [`PackageInfo::from_str`].
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use std::{fs::File, io::Write};
183    ///
184    /// use alpm_common::MetadataFile;
185    /// use alpm_pkginfo::{PackageInfo, PackageInfoSchema};
186    /// use alpm_types::{SchemaVersion, semver_version::Version};
187    ///
188    /// # fn main() -> testresult::TestResult {
189    /// let pkginfo_v2_data = r#"pkgname = example
190    /// pkgbase = example
191    /// xdata = pkgtype=pkg
192    /// pkgver = 1:1.0.0-1
193    /// pkgdesc = A project that does something
194    /// url = https://example.org/
195    /// builddate = 1729181726
196    /// packager = John Doe <john@example.org>
197    /// size = 181849963
198    /// arch = any
199    /// "#;
200    ///
201    /// let pkginfo_v2 = PackageInfo::from_str_with_schema(
202    ///     pkginfo_v2_data,
203    ///     Some(PackageInfoSchema::V2(SchemaVersion::new(Version::new(
204    ///         2, 0, 0,
205    ///     )))),
206    /// )?;
207    /// assert_eq!(pkginfo_v2.to_string(), pkginfo_v2_data);
208    ///
209    /// let pkginfo_v1_data = r#"pkgname = example
210    /// pkgbase = example
211    /// pkgver = 1:1.0.0-1
212    /// pkgdesc = A project that does something
213    /// url = https://example.org/
214    /// builddate = 1729181726
215    /// packager = John Doe <john@example.org>
216    /// size = 181849963
217    /// arch = any
218    /// "#;
219    ///
220    /// let pkginfo_v1 = PackageInfo::from_str_with_schema(
221    ///     pkginfo_v1_data,
222    ///     Some(PackageInfoSchema::V1(SchemaVersion::new(Version::new(
223    ///         1, 0, 0,
224    ///     )))),
225    /// )?;
226    /// assert_eq!(pkginfo_v1.to_string(), pkginfo_v1_data);
227    /// # Ok(())
228    /// # }
229    /// ```
230    ///
231    /// # Errors
232    ///
233    /// Returns an error if
234    /// - `schema` is [`Some`] and the specified variant of [`PackageInfo`] cannot be constructed
235    ///   from `s`,
236    /// - `schema` is [`None`] and
237    ///   - a [`PackageInfoSchema`] cannot be derived from `s`,
238    ///   - or the detected variant of [`PackageInfo`] cannot be constructed from `s`.
239    fn from_str_with_schema(s: &str, schema: Option<PackageInfoSchema>) -> Result<Self, Error> {
240        let schema = match schema {
241            Some(schema) => schema,
242            None => PackageInfoSchema::derive_from_str(s)?,
243        };
244
245        match schema {
246            PackageInfoSchema::V1(_) => Ok(PackageInfo::V1(PackageInfoV1::from_str(s)?)),
247            PackageInfoSchema::V2(_) => Ok(PackageInfo::V2(PackageInfoV2::from_str(s)?)),
248        }
249    }
250}
251
252impl Display for PackageInfo {
253    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
254        write!(
255            f,
256            "{}",
257            match self {
258                Self::V1(pkginfo) => pkginfo.to_string(),
259                Self::V2(pkginfo) => pkginfo.to_string(),
260            },
261        )
262    }
263}
264
265impl FromStr for PackageInfo {
266    type Err = Error;
267
268    /// Creates a [`PackageInfo`] from string slice `s`.
269    ///
270    /// Calls [`PackageInfo::from_str_with_schema`] with `schema` set to [`None`].
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if
275    /// - a [`PackageInfoSchema`] cannot be derived from `s`,
276    /// - or the detected variant of [`PackageInfo`] cannot be constructed from `s`.
277    fn from_str(s: &str) -> Result<Self, Self::Err> {
278        Self::from_str_with_schema(s, None)
279    }
280}