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