1use std::{convert::Infallible, fmt::Display, str::FromStr};
2
3use alpm_parsers::{
4 iter_str_context,
5 traits::{AlpmParser, ParserUntil},
6};
7use serde::{Deserialize, Serialize};
8use serde_with::{DeserializeFromStr, SerializeDisplay};
9use strum::{Display, EnumString, VariantNames};
10use winnow::{
11 ModalResult,
12 Parser,
13 ascii::{alpha1, space0},
14 combinator::{alt, not, peek, repeat_till},
15 error::{ContextError, ErrMode, StrContext, StrContextValue},
16 token::any,
17};
18
19use crate::{Error, Name};
20
21#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq, Serialize, VariantNames)]
39pub enum PackageType {
40 #[strum(to_string = "debug")]
42 Debug,
43 #[strum(to_string = "pkg")]
45 Package,
46 #[strum(to_string = "src")]
48 Source,
49 #[strum(to_string = "split")]
51 Split,
52}
53
54impl AlpmParser for PackageType {
55 fn parser(input: &mut &str) -> Result<Self, ErrMode<ContextError>> {
62 alpha1
63 .try_map(PackageType::from_str)
64 .context(StrContext::Label("package type"))
65 .context_with(iter_str_context!([PackageType::VARIANTS]))
66 .parse_next(input)
67 }
68
69 fn delimiter_error_context<'a, O, P>(
70 parser: P,
71 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
72 where
73 P: Parser<&'a str, O, ErrMode<ContextError>>,
74 {
75 parser
76 .context(StrContext::Label("package type"))
77 .context(StrContext::Expected(StrContextValue::Description(
78 "a string consisting of alphabetic characters",
79 )))
80 }
81}
82
83#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
105pub struct PackageDescription(String);
106
107impl PackageDescription {
108 pub fn new(description: &str) -> Self {
110 Self::from(description)
111 }
112}
113
114impl Default for PackageDescription {
115 fn default() -> Self {
120 Self::new("")
121 }
122}
123
124impl FromStr for PackageDescription {
125 type Err = Infallible;
126
127 fn from_str(s: &str) -> Result<Self, Self::Err> {
128 Ok(Self::from(s))
129 }
130}
131
132impl AsRef<str> for PackageDescription {
133 fn as_ref(&self) -> &str {
135 &self.0
136 }
137}
138
139impl From<&str> for PackageDescription {
140 fn from(value: &str) -> Self {
146 let mut description = value.trim().replace(['\n', '\r', '\t'], " ");
148
149 let mut previous = ' ';
151 description.retain(|ch| {
152 if ch == ' ' && previous == ' ' {
153 return false;
154 };
155 previous = ch;
156 true
157 });
158
159 Self(description)
160 }
161}
162
163impl Display for PackageDescription {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 write!(f, "{}", self.0)
166 }
167}
168
169pub type PackageBaseName = Name;
190
191#[derive(Clone, Debug, DeserializeFromStr, PartialEq, SerializeDisplay)]
195pub struct ExtraDataEntry {
196 key: String,
197 value: String,
198}
199
200impl ExtraDataEntry {
201 pub fn new(key: String, value: String) -> Self {
203 Self { key, value }
204 }
205
206 pub fn key(&self) -> &str {
208 &self.key
209 }
210
211 pub fn value(&self) -> &str {
213 &self.value
214 }
215}
216
217impl ParserUntil for ExtraDataEntry {
218 fn parser_until<'a, P>(delimiter: P) -> impl Parser<&'a str, Self, ErrMode<ContextError>>
225 where
226 P: Parser<&'a str, &'a str, ErrMode<ContextError>>,
227 {
228 let mut delimiter_parser = delimiter;
231 move |input: &mut &'a str| -> ModalResult<Self> {
232 not("=")
234 .context(StrContext::Label("extra data"))
235 .context(StrContext::Expected(StrContextValue::Description(
236 "a utf-8 key before the `=` delimiter",
237 )))
238 .parse_next(input)?;
239
240 let key: &str = repeat_till::<_, _, (), _, _, _, _>(
241 1..,
242 any,
243 peek(alt((
244 (space0, "=", space0).take(),
245 delimiter_parser.by_ref(),
246 ))),
247 )
248 .take()
249 .context(StrContext::Label("extra data key"))
250 .context(StrContext::Expected(StrContextValue::Description(
251 "a UTF-8 string, followed by an equals (`=`) character.",
252 )))
253 .parse_next(input)?;
254
255 (space0, "=", space0)
256 .context(StrContext::Label("extra data delimiter"))
257 .context(StrContext::Expected(StrContextValue::Description(
258 "a `=` between the key and value",
259 )))
260 .parse_next(input)?;
261
262 let value: &str =
263 repeat_till::<_, _, (), _, _, _, _>(1.., any, peek(delimiter_parser.by_ref()))
264 .take()
265 .context(StrContext::Label("extra data value"))
266 .context(StrContext::Expected(StrContextValue::Description(
267 "a UTF-8 string",
268 )))
269 .parse_next(input)?;
270
271 peek(delimiter_parser.by_ref())
272 .context(StrContext::Label("extra data value"))
273 .context(StrContext::Expected(StrContextValue::Description(
274 "end of input",
275 )))
276 .parse_next(input)?;
277
278 Ok(Self::new(key.trim().to_string(), value.trim().to_string()))
279 }
280 }
281}
282
283impl FromStr for ExtraDataEntry {
284 type Err = Error;
285
286 fn from_str(s: &str) -> Result<Self, Self::Err> {
310 Ok(Self::parser_until_eof.parse(s)?)
311 }
312}
313
314impl Display for ExtraDataEntry {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316 write!(f, "{}={}", self.key, self.value)
317 }
318}
319
320#[derive(Clone, Debug, PartialEq, Serialize)]
327pub struct ExtraData(Vec<ExtraDataEntry>);
328
329impl ExtraData {
330 pub fn pkg_type(&self) -> PackageType {
332 self.0
333 .iter()
334 .find(|v| v.key() == "pkgtype")
335 .map(|v| PackageType::from_str(v.value()).expect("Invalid package type"))
336 .unwrap_or_else(|| unreachable!("Valid xdata should always contain a pkgtype entry."))
337 }
338
339 pub fn len(&self) -> usize {
341 self.0.len()
342 }
343
344 pub fn is_empty(&self) -> bool {
349 self.0.is_empty()
350 }
351}
352
353impl TryFrom<Vec<ExtraDataEntry>> for ExtraData {
354 type Error = Error;
355
356 fn try_from(value: Vec<ExtraDataEntry>) -> Result<Self, Self::Error> {
365 if let Some(pkg_type) = value.iter().find(|v| v.key() == "pkgtype") {
366 let _ = PackageType::from_str(pkg_type.value())?;
367 Ok(Self(value))
368 } else {
369 Err(Error::MissingComponent {
370 component: "extra_data with a valid \"pkgtype\" entry",
371 })
372 }
373 }
374}
375
376impl TryFrom<ExtraDataEntry> for ExtraData {
377 type Error = Error;
378
379 fn try_from(value: ExtraDataEntry) -> Result<Self, Self::Error> {
387 Self::try_from(vec![value])
388 }
389}
390
391impl From<ExtraData> for Vec<ExtraDataEntry> {
392 fn from(value: ExtraData) -> Self {
394 value.0
395 }
396}
397
398impl IntoIterator for ExtraData {
399 type Item = ExtraDataEntry;
400 type IntoIter = std::vec::IntoIter<ExtraDataEntry>;
401
402 fn into_iter(self) -> Self::IntoIter {
404 self.0.into_iter()
405 }
406}
407
408impl AsRef<[ExtraDataEntry]> for ExtraData {
409 fn as_ref(&self) -> &[ExtraDataEntry] {
411 &self.0
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use std::str::FromStr;
418
419 use insta::assert_snapshot;
420 use rstest::rstest;
421 use testresult::TestResult;
422
423 use super::*;
424 use crate::configure_insta;
425
426 #[rstest]
427 #[case("debug", Ok(PackageType::Debug))]
428 #[case("pkg", Ok(PackageType::Package))]
429 #[case("src", Ok(PackageType::Source))]
430 #[case("split", Ok(PackageType::Split))]
431 #[case("foo", Err(strum::ParseError::VariantNotFound))]
432 fn pkgtype_from_string(
433 #[case] from_str: &str,
434 #[case] result: Result<PackageType, strum::ParseError>,
435 ) {
436 assert_eq!(PackageType::from_str(from_str), result);
437 }
438
439 #[rstest]
440 #[case(PackageType::Debug, "debug")]
441 #[case(PackageType::Package, "pkg")]
442 #[case(PackageType::Source, "src")]
443 #[case(PackageType::Split, "split")]
444 fn pkgtype_format_string(#[case] pkgtype: PackageType, #[case] pkgtype_str: &str) {
445 assert_eq!(pkgtype_str, format!("{pkgtype}"));
446 }
447
448 #[rstest]
449 #[case("key=value", "key", "value")]
450 #[case("pkgtype=debug", "pkgtype", "debug")]
451 #[case("test-123@.foo_+=1000", "test-123@.foo_+", "1000")]
452 fn extra_data_entry_from_str(
453 #[case] data: &str,
454 #[case] key: &str,
455 #[case] value: &str,
456 ) -> TestResult {
457 let extra_data = ExtraDataEntry::from_str(data)?;
458 assert_eq!(extra_data.key(), key);
459 assert_eq!(extra_data.value(), value);
460 assert_eq!(extra_data.to_string(), data);
461 Ok(())
462 }
463
464 #[rstest]
465 #[case("key")]
466 #[case("key=")]
467 #[case("=value")]
468 fn extra_data_entry_from_str_error(#[case] input: &str) {
469 let Err(Error::ParseError(err_msg)) = ExtraDataEntry::from_str(input) else {
470 panic!("'{input}' erroneously parsed as a ExtraDataEntry")
471 };
472
473 let (test_name, _guard) = configure_insta();
474 assert_snapshot!(test_name, err_msg.to_string());
475 }
476
477 #[rstest]
478 #[case::empty_list(vec![])]
479 #[case::invalid_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=foo")?])]
480 fn extra_data_invalid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
481 assert!(ExtraData::try_from(xdata).is_err());
482 Ok(())
483 }
484
485 #[rstest]
486 #[case::only_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=pkg")?])]
487 #[case::with_additional_xdata_entry(vec![ExtraDataEntry::from_str("pkgtype=pkg")?, ExtraDataEntry::from_str("foo=bar")?])]
488 fn extra_data_valid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
489 let xdata = ExtraData::try_from(xdata)?;
490 assert_eq!(xdata.pkg_type(), PackageType::Package);
491 Ok(())
492 }
493
494 #[rstest]
495 #[case(" trailing ", "trailing")]
496 #[case("in between words", "in between words")]
497 #[case("\nsome\t whitespace\n chars\n", "some whitespace chars")]
498 #[case(" \neverything\t combined\n yeah \n ", "everything combined yeah")]
499 fn package_description(#[case] input: &str, #[case] result: &str) {
500 assert_eq!(PackageDescription::new(input).to_string(), result);
501 }
502}