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