alpm_db/desc/file.rs
1use std::{
2 fmt::Display,
3 fs::File,
4 path::{Path, PathBuf},
5 str::FromStr,
6};
7
8use alpm_common::{FileFormatSchema, MetadataFile};
9
10use crate::{
11 Error,
12 desc::{DbDescFileV1, DbDescFileV2, DbDescSchema},
13};
14
15/// A representation of the [alpm-db-desc] file format.
16///
17/// Tracks all supported schema versions (`v1` and `v2`) of the database description file.
18/// Each variant corresponds to a distinct layout of the format.
19///
20/// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
21#[derive(Clone, Debug, PartialEq, serde::Serialize)]
22#[serde(untagged)]
23pub enum DbDescFile {
24 /// The [alpm-db-descv1] file format.
25 ///
26 /// [alpm-db-descv1]: https://alpm.archlinux.page/specifications/alpm-db-descv1.5.html
27 V1(DbDescFileV1),
28 /// The [alpm-db-descv2] file format.
29 ///
30 /// This revision of the file format, adds the `%XDATA%` section.
31 ///
32 /// [alpm-db-descv2]: https://alpm.archlinux.page/specifications/alpm-db-descv2.5.html
33 V2(DbDescFileV2),
34}
35
36impl MetadataFile<DbDescSchema> for DbDescFile {
37 type Err = Error;
38
39 /// Creates a [`DbDescFile`] from a file on disk, optionally validated using a [`DbDescSchema`].
40 ///
41 /// Opens the file and defers to [`DbDescFile::from_reader_with_schema`].
42 ///
43 /// # Examples
44 ///
45 /// ```
46 /// use std::{fs::File, io::Write};
47 ///
48 /// use alpm_common::{FileFormatSchema, MetadataFile};
49 /// use alpm_db::desc::{DbDescFile, DbDescSchema};
50 /// use alpm_types::{SchemaVersion, semver_version::Version};
51 ///
52 /// # fn main() -> testresult::TestResult {
53 /// // Prepare a file with DB desc data (v1)
54 /// let (file, desc_data) = {
55 /// let desc_data = r#"%NAME%
56 /// foo
57 ///
58 /// %VERSION%
59 /// 1.0.0-1
60 ///
61 /// %BASE%
62 /// foo
63 ///
64 /// %DESC%
65 /// An example package
66 ///
67 /// %URL%
68 /// https://example.org/
69 ///
70 /// %ARCH%
71 /// x86_64
72 ///
73 /// %BUILDDATE%
74 /// 1733737242
75 ///
76 /// %INSTALLDATE%
77 /// 1733737243
78 ///
79 /// %PACKAGER%
80 /// Foobar McFooface <foobar@mcfooface.org>
81 ///
82 /// %SIZE%
83 /// 123
84 ///
85 /// %VALIDATION%
86 /// pgp
87 ///
88 /// "#;
89 /// let file = tempfile::NamedTempFile::new()?;
90 /// let mut output = File::create(&file)?;
91 /// write!(output, "{}", desc_data)?;
92 /// (file, desc_data)
93 /// };
94 ///
95 /// let db_desc = DbDescFile::from_file_with_schema(
96 /// file.path(),
97 /// Some(DbDescSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
98 /// )?;
99 /// assert_eq!(db_desc.to_string(), desc_data);
100 /// # Ok(())
101 /// # }
102 /// ```
103 ///
104 /// # Errors
105 ///
106 /// Returns an error if:
107 ///
108 /// - the file cannot be opened for reading,
109 /// - the contents cannot be parsed into any known [`DbDescFile`] variant,
110 /// - or the provided [`DbDescSchema`] does not match the contents of the file.
111 fn from_file_with_schema(
112 file: impl AsRef<Path>,
113 schema: Option<DbDescSchema>,
114 ) -> Result<Self, Error> {
115 let file = file.as_ref();
116 Self::from_reader_with_schema(
117 File::open(file).map_err(|source| Error::IoPathError {
118 path: PathBuf::from(file),
119 context: "opening the file for reading",
120 source,
121 })?,
122 schema,
123 )
124 }
125
126 /// Creates a [`DbDescFile`] from any readable stream, optionally validated using a
127 /// [`DbDescSchema`].
128 ///
129 /// Reads the `reader` to a string buffer and defers to [`DbDescFile::from_str_with_schema`].
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// use std::{fs::File, io::Write};
135 ///
136 /// use alpm_common::MetadataFile;
137 /// use alpm_db::desc::{DbDescFile, DbDescSchema};
138 /// use alpm_types::{SchemaVersion, semver_version::Version};
139 ///
140 /// # fn main() -> testresult::TestResult {
141 /// // Prepare a reader with DB desc data (v2)
142 /// let (reader, desc_data) = {
143 /// let desc_data = r#"%NAME%
144 /// foo
145 ///
146 /// %VERSION%
147 /// 1.0.0-1
148 ///
149 /// %BASE%
150 /// foo
151 ///
152 /// %DESC%
153 /// An example package
154 ///
155 /// %URL%
156 /// https://example.org/
157 ///
158 /// %ARCH%
159 /// x86_64
160 ///
161 /// %BUILDDATE%
162 /// 1733737242
163 ///
164 /// %INSTALLDATE%
165 /// 1733737243
166 ///
167 /// %PACKAGER%
168 /// Foobar McFooface <foobar@mcfooface.org>
169 ///
170 /// %SIZE%
171 /// 123
172 ///
173 /// %VALIDATION%
174 /// pgp
175 ///
176 /// %XDATA%
177 /// pkgtype=pkg
178 ///
179 /// "#;
180 /// let file = tempfile::NamedTempFile::new()?;
181 /// let mut output = File::create(&file)?;
182 /// write!(output, "{}", desc_data)?;
183 /// (File::open(&file.path())?, desc_data)
184 /// };
185 ///
186 /// let db_desc = DbDescFile::from_reader_with_schema(
187 /// reader,
188 /// Some(DbDescSchema::V2(SchemaVersion::new(Version::new(2, 0, 0)))),
189 /// )?;
190 /// assert_eq!(db_desc.to_string(), desc_data);
191 /// # Ok(())
192 /// # }
193 /// ```
194 ///
195 /// # Errors
196 ///
197 /// Returns an error if:
198 ///
199 /// - the `reader` cannot be read to string,
200 /// - the data cannot be parsed into a known [`DbDescFile`] variant,
201 /// - or the provided [`DbDescSchema`] does not match the parsed content.
202 fn from_reader_with_schema(
203 mut reader: impl std::io::Read,
204 schema: Option<DbDescSchema>,
205 ) -> Result<Self, Error> {
206 let mut buf = String::new();
207 reader
208 .read_to_string(&mut buf)
209 .map_err(|source| Error::IoReadError {
210 context: "reading DB desc data",
211 source,
212 })?;
213 Self::from_str_with_schema(&buf, schema)
214 }
215
216 /// Creates a [`DbDescFile`] from a string slice, optionally validated using a [`DbDescSchema`].
217 ///
218 /// If `schema` is [`None`], automatically infers the schema version by inspecting the input
219 /// (`v1` = no `%XDATA%` section, `v2` = has `%XDATA%`).
220 ///
221 /// # Examples
222 ///
223 /// ```
224 /// use alpm_common::MetadataFile;
225 /// use alpm_db::desc::{DbDescFile, DbDescSchema};
226 /// use alpm_types::{SchemaVersion, semver_version::Version};
227 ///
228 /// # fn main() -> testresult::TestResult {
229 /// let v1_data = r#"%NAME%
230 /// foo
231 ///
232 /// %VERSION%
233 /// 1.0.0-1
234 ///
235 /// %BASE%
236 /// foo
237 ///
238 /// %DESC%
239 /// An example package
240 ///
241 /// %URL%
242 /// https://example.org/
243 ///
244 /// %ARCH%
245 /// x86_64
246 ///
247 /// %BUILDDATE%
248 /// 1733737242
249 ///
250 /// %INSTALLDATE%
251 /// 1733737243
252 ///
253 /// %PACKAGER%
254 /// Foobar McFooface <foobar@mcfooface.org>
255 ///
256 /// %SIZE%
257 /// 123
258 ///
259 /// %VALIDATION%
260 /// pgp
261 ///
262 /// "#;
263 ///
264 /// let dbdesc_v1 = DbDescFile::from_str_with_schema(
265 /// v1_data,
266 /// Some(DbDescSchema::V1(SchemaVersion::new(Version::new(1, 0, 0)))),
267 /// )?;
268 /// assert_eq!(dbdesc_v1.to_string(), v1_data);
269 ///
270 /// let v2_data = r#"%NAME%
271 /// foo
272 ///
273 /// %VERSION%
274 /// 1.0.0-1
275 ///
276 /// %BASE%
277 /// foo
278 ///
279 /// %DESC%
280 /// An example package
281 ///
282 /// %URL%
283 /// https://example.org/
284 ///
285 /// %ARCH%
286 /// x86_64
287 ///
288 /// %BUILDDATE%
289 /// 1733737242
290 ///
291 /// %INSTALLDATE%
292 /// 1733737243
293 ///
294 /// %PACKAGER%
295 /// Foobar McFooface <foobar@mcfooface.org>
296 ///
297 /// %SIZE%
298 /// 123
299 ///
300 /// %VALIDATION%
301 /// pgp
302 ///
303 /// %XDATA%
304 /// pkgtype=pkg
305 ///
306 /// "#;
307 ///
308 /// let dbdesc_v2 = DbDescFile::from_str_with_schema(
309 /// v2_data,
310 /// Some(DbDescSchema::V2(SchemaVersion::new(Version::new(2, 0, 0)))),
311 /// )?;
312 /// assert_eq!(dbdesc_v2.to_string(), v2_data);
313 /// # Ok(())
314 /// # }
315 /// ```
316 ///
317 /// # Errors
318 ///
319 /// Returns an error if:
320 ///
321 /// - the input cannot be parsed into a valid [`DbDescFile`],
322 /// - or the derived or provided schema does not match the detected format.
323 fn from_str_with_schema(s: &str, schema: Option<DbDescSchema>) -> Result<Self, Error> {
324 let schema = match schema {
325 Some(schema) => schema,
326 None => DbDescSchema::derive_from_str(s)?,
327 };
328
329 match schema {
330 DbDescSchema::V1(_) => Ok(DbDescFile::V1(DbDescFileV1::from_str(s)?)),
331 DbDescSchema::V2(_) => Ok(DbDescFile::V2(DbDescFileV2::from_str(s)?)),
332 }
333 }
334}
335
336impl Display for DbDescFile {
337 /// Returns the textual representation of the [`DbDescFile`] in its corresponding
338 /// [alpm-db-desc] format.
339 ///
340 /// [alpm-db-desc]: https://alpm.archlinux.page/specifications/alpm-db-desc.5.html
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 match self {
343 Self::V1(file) => write!(f, "{file}"),
344 Self::V2(file) => write!(f, "{file}"),
345 }
346 }
347}
348
349impl FromStr for DbDescFile {
350 type Err = Error;
351
352 /// Creates a [`DbDescFile`] from a string slice.
353 ///
354 /// Internally calls [`DbDescFile::from_str_with_schema`] with `schema` set to [`None`].
355 ///
356 /// # Errors
357 ///
358 /// Returns an error if [`DbDescFile::from_str_with_schema`] fails.
359 fn from_str(s: &str) -> Result<Self, Self::Err> {
360 Self::from_str_with_schema(s, None)
361 }
362}