1use std::{convert::Infallible, fmt::Display, str::FromStr};
2
3use alpm_parsers::prelude::*;
4#[cfg(feature = "serde")]
5use serde::Serialize;
6#[cfg(feature = "serde")]
7use serde_with::{DeserializeFromStr, SerializeDisplay};
8use strum::{Display, EnumString, VariantNames};
9use winnow::{
10 ascii::{alpha1, space0},
11 combinator::{alt, not, peek, repeat_till},
12 error::ErrMode,
13 token::any,
14};
15
16use crate::{Error, Name};
17
18#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq, VariantNames)]
36#[cfg_attr(feature = "serde", derive(Serialize))]
37pub enum PackageType {
38 #[strum(to_string = "debug")]
40 Debug,
41 #[strum(to_string = "pkg")]
43 Package,
44 #[strum(to_string = "src")]
46 Source,
47 #[strum(to_string = "split")]
49 Split,
50}
51
52impl AlpmParser for PackageType {
53 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
60 alpha1
61 .try_map(PackageType::from_str)
62 .expected_strings(PackageType::VARIANTS)
63 .layer("package type")
64 .parse_next(input)
65 }
66
67 fn delimiter_error_context<'a, O, P>(
68 parser: P,
69 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
70 where
71 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
72 {
73 parser
74 .expected_text("a string consisting of alphabetic characters")
75 .layer("package type")
76 }
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
101#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
102pub struct PackageDescription(String);
103
104impl PackageDescription {
105 pub fn new(description: &str) -> Self {
107 Self::from(description)
108 }
109}
110
111impl Default for PackageDescription {
112 fn default() -> Self {
117 Self::new("")
118 }
119}
120
121impl FromStr for PackageDescription {
122 type Err = Infallible;
123
124 fn from_str(s: &str) -> Result<Self, Self::Err> {
125 Ok(Self::from(s))
126 }
127}
128
129impl AsRef<str> for PackageDescription {
130 fn as_ref(&self) -> &str {
132 &self.0
133 }
134}
135
136impl From<&str> for PackageDescription {
137 fn from(value: &str) -> Self {
143 let mut description = value.trim().replace(['\n', '\r', '\t'], " ");
145
146 let mut previous = ' ';
148 description.retain(|ch| {
149 if ch == ' ' && previous == ' ' {
150 return false;
151 };
152 previous = ch;
153 true
154 });
155
156 Self(description)
157 }
158}
159
160impl Display for PackageDescription {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(f, "{}", self.0)
163 }
164}
165
166pub type PackageBaseName = Name;
187
188#[derive(Clone, Debug, PartialEq)]
192#[cfg_attr(feature = "serde", derive(DeserializeFromStr, SerializeDisplay))]
193pub struct ExtraDataEntry {
194 key: String,
195 value: String,
196}
197
198impl ExtraDataEntry {
199 pub fn new(key: String, value: String) -> Self {
201 Self { key, value }
202 }
203
204 pub fn key(&self) -> &str {
206 &self.key
207 }
208
209 pub fn value(&self) -> &str {
211 &self.value
212 }
213}
214
215impl ParserUntil for ExtraDataEntry {
216 fn parser_until<'a, P>(delimiter: P) -> impl Parser<Input<'a>, Self, ErrMode<ParseStack<'a>>>
223 where
224 P: Parser<Input<'a>, &'a str, ErrMode<ParseStack<'a>>>,
225 {
226 let mut delimiter_parser = delimiter;
229 let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
230 not("=")
232 .label("extra data")
233 .expected_text("a UTF-8 key before the `=` delimiter")
234 .parse_next(input)?;
235
236 let key: &str = repeat_till::<_, _, (), _, _, _, _>(
237 1..,
238 any,
239 peek(alt((
240 (space0, "=", space0).take(),
241 delimiter_parser.by_ref(),
242 ))),
243 )
244 .take()
245 .label("extra data key")
246 .expected_text("a UTF-8 string, followed by an equals (`=`) character.")
247 .parse_next(input)?;
248
249 (space0, "=", space0)
250 .label("extra data delimiter")
251 .expected_text("a `=` between the key and value")
252 .parse_next(input)?;
253
254 let value: &str =
255 repeat_till::<_, _, (), _, _, _, _>(1.., any, peek(delimiter_parser.by_ref()))
256 .take()
257 .label("extra data value")
258 .expected_text("a UTF-8 string")
259 .parse_next(input)?;
260
261 peek(delimiter_parser.by_ref())
262 .label("extra data value")
263 .expected_text("end of input")
264 .parse_next(input)?;
265
266 Ok(Self::new(key.trim().to_string(), value.trim().to_string()))
267 };
268
269 parser.layer("extra-data entry")
270 }
271}
272
273impl FromStr for ExtraDataEntry {
274 type Err = Error;
275
276 fn from_str(s: &str) -> Result<Self, Self::Err> {
300 Ok(Self::parser_until_eof.parse(Input::new(s))?)
301 }
302}
303
304impl Display for ExtraDataEntry {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 write!(f, "{}={}", self.key, self.value)
307 }
308}
309
310#[derive(Clone, Debug, PartialEq)]
317#[cfg_attr(feature = "serde", derive(Serialize))]
318pub struct ExtraData(Vec<ExtraDataEntry>);
319
320impl ExtraData {
321 #[expect(clippy::expect_used)]
323 pub fn pkg_type(&self) -> PackageType {
324 self.0
325 .iter()
326 .find(|v| v.key() == "pkgtype")
327 .map(|v| {
328 PackageType::from_str(v.value())
329 .expect("a valid xdata entry contains a valid package type")
330 })
331 .unwrap_or_else(|| unreachable!("Valid xdata should always contain a pkgtype entry."))
332 }
333
334 pub fn len(&self) -> usize {
336 self.0.len()
337 }
338
339 pub fn is_empty(&self) -> bool {
344 self.0.is_empty()
345 }
346}
347
348impl TryFrom<Vec<ExtraDataEntry>> for ExtraData {
349 type Error = Error;
350
351 fn try_from(value: Vec<ExtraDataEntry>) -> Result<Self, Self::Error> {
360 if let Some(pkg_type) = value.iter().find(|v| v.key() == "pkgtype") {
361 let _ = PackageType::from_str(pkg_type.value())?;
362 Ok(Self(value))
363 } else {
364 Err(Error::MissingComponent {
365 component: "extra_data with a valid \"pkgtype\" entry",
366 })
367 }
368 }
369}
370
371impl TryFrom<ExtraDataEntry> for ExtraData {
372 type Error = Error;
373
374 fn try_from(value: ExtraDataEntry) -> Result<Self, Self::Error> {
382 Self::try_from(vec![value])
383 }
384}
385
386impl From<ExtraData> for Vec<ExtraDataEntry> {
387 fn from(value: ExtraData) -> Self {
389 value.0
390 }
391}
392
393impl IntoIterator for ExtraData {
394 type Item = ExtraDataEntry;
395 type IntoIter = std::vec::IntoIter<ExtraDataEntry>;
396
397 fn into_iter(self) -> Self::IntoIter {
399 self.0.into_iter()
400 }
401}
402
403impl AsRef<[ExtraDataEntry]> for ExtraData {
404 fn as_ref(&self) -> &[ExtraDataEntry] {
406 &self.0
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use std::str::FromStr;
413
414 use insta::assert_snapshot;
415 use rstest::rstest;
416 use testresult::TestResult;
417
418 use super::*;
419 use crate::configure_insta;
420
421 #[rstest]
422 #[case("debug", Ok(PackageType::Debug))]
423 #[case("pkg", Ok(PackageType::Package))]
424 #[case("src", Ok(PackageType::Source))]
425 #[case("split", Ok(PackageType::Split))]
426 #[case("foo", Err(strum::ParseError::VariantNotFound))]
427 fn pkgtype_from_string(
428 #[case] from_str: &str,
429 #[case] result: Result<PackageType, strum::ParseError>,
430 ) {
431 assert_eq!(PackageType::from_str(from_str), result);
432 }
433
434 #[rstest]
435 #[case(PackageType::Debug, "debug")]
436 #[case(PackageType::Package, "pkg")]
437 #[case(PackageType::Source, "src")]
438 #[case(PackageType::Split, "split")]
439 fn pkgtype_format_string(#[case] pkgtype: PackageType, #[case] pkgtype_str: &str) {
440 assert_eq!(pkgtype_str, format!("{pkgtype}"));
441 }
442
443 #[rstest]
444 #[case("key=value", "key", "value")]
445 #[case("pkgtype=debug", "pkgtype", "debug")]
446 #[case("test-123@.foo_+=1000", "test-123@.foo_+", "1000")]
447 fn extra_data_entry_from_str(
448 #[case] data: &str,
449 #[case] key: &str,
450 #[case] value: &str,
451 ) -> TestResult {
452 let extra_data = ExtraDataEntry::from_str(data)?;
453 assert_eq!(extra_data.key(), key);
454 assert_eq!(extra_data.value(), value);
455 assert_eq!(extra_data.to_string(), data);
456 Ok(())
457 }
458
459 #[rstest]
460 #[case("key")]
461 #[case("key=")]
462 #[case("=value")]
463 fn extra_data_entry_from_str_error(#[case] input: &str) {
464 let (test_name, _guard) = configure_insta();
465
466 let Err(Error::ParseError(err_msg)) = ExtraDataEntry::from_str(input) else {
467 panic!("'{input}' erroneously parsed as a ExtraDataEntry")
468 };
469
470 assert_snapshot!(test_name, err_msg.to_string());
471 }
472
473 #[rstest]
474 #[case::empty_list(vec![])]
475 #[case::invalid_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=foo")?])]
476 fn extra_data_invalid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
477 assert!(ExtraData::try_from(xdata).is_err());
478 Ok(())
479 }
480
481 #[rstest]
482 #[case::only_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=pkg")?])]
483 #[case::with_additional_xdata_entry(vec![ExtraDataEntry::from_str("pkgtype=pkg")?, ExtraDataEntry::from_str("foo=bar")?])]
484 fn extra_data_valid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
485 let xdata = ExtraData::try_from(xdata)?;
486 assert_eq!(xdata.pkg_type(), PackageType::Package);
487 Ok(())
488 }
489
490 #[rstest]
491 #[case(" trailing ", "trailing")]
492 #[case("in between words", "in between words")]
493 #[case("\nsome\t whitespace\n chars\n", "some whitespace chars")]
494 #[case(" \neverything\t combined\n yeah \n ", "everything combined yeah")]
495 fn package_description(#[case] input: &str, #[case] result: &str) {
496 assert_eq!(PackageDescription::new(input).to_string(), result);
497 }
498}