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