1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4 string::ToString,
5};
6
7use alpm_parsers::{
8 iter_char_context,
9 traits::{AlpmParser, ParserUntil},
10};
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13use winnow::{
14 ModalResult,
15 Parser,
16 combinator::{Repeat, alt, eof, peek, repeat, repeat_till},
17 error::{ContextError, ErrMode, StrContext, StrContextValue},
18 token::one_of,
19};
20
21use crate::Error;
22
23#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
51pub struct BuildTool(Name);
52
53impl BuildTool {
54 pub fn new(name: Name) -> Self {
56 BuildTool(name)
57 }
58
59 pub fn new_with_restriction(name: &str, restrictions: &[Name]) -> Result<Self, Error> {
72 let buildtool = BuildTool::from_str(name)?;
73 if buildtool.matches_restriction(restrictions) {
74 Ok(buildtool)
75 } else {
76 Err(Error::ValueDoesNotMatchRestrictions {
77 restrictions: restrictions.iter().map(ToString::to_string).collect(),
78 })
79 }
80 }
81
82 pub fn matches_restriction(&self, restrictions: &[Name]) -> bool {
84 restrictions
85 .iter()
86 .any(|restriction| restriction.eq(self.inner()))
87 }
88
89 pub fn inner(&self) -> &Name {
91 &self.0
92 }
93}
94
95impl FromStr for BuildTool {
96 type Err = Error;
97 fn from_str(s: &str) -> Result<BuildTool, Self::Err> {
99 Name::new(s).map(BuildTool)
100 }
101}
102
103impl Display for BuildTool {
104 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
105 write!(fmt, "{}", self.inner())
106 }
107}
108
109#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
136#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
137pub struct Name(String);
138
139impl Name {
140 const SPECIAL_FIRST_CHARS: [char; 3] = ['_', '@', '+'];
142 const NEVER_FIRST_CHAR: [char; 5] = ['_', '@', '+', '-', '.'];
144
145 pub fn new(name: &str) -> Result<Self, Error> {
147 Self::from_str(name)
148 }
149
150 pub fn inner(&self) -> &str {
152 &self.0
153 }
154}
155
156impl Name {
157 pub(crate) fn parse_name_followed_by_version<'a>(
181 delimiter_count: usize,
182 ) -> impl Parser<&'a str, Self, ErrMode<ContextError>> {
183 let never_first_char_list = ['_', '@', '+', '.'];
184
185 let alphanum = |c: char| c.is_ascii_alphanumeric();
186 let first_char = one_of((alphanum, Self::SPECIAL_FIRST_CHARS))
187 .context(StrContext::Label("first character of package name"))
188 .context(StrContext::Expected(StrContextValue::Description(
189 "ASCII alphanumeric character",
190 )))
191 .context_with(iter_char_context!(Self::SPECIAL_FIRST_CHARS));
192
193 let never_first_char = one_of((alphanum, never_first_char_list));
194
195 let part: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
207 let parts: Repeat<_, _, _, (), _> = repeat(
208 delimiter_count - 1,
209 (
210 part,
211 '-'.context(StrContext::Label("character in package name"))
212 .context(StrContext::Expected(StrContextValue::Description(
213 "ASCII alphanumeric character",
214 )))
215 .context_with(iter_char_context!(Self::NEVER_FIRST_CHAR)),
216 ),
217 );
218
219 let alphanum = |c: char| c.is_ascii_alphanumeric();
221 let never_first_char = one_of((alphanum, never_first_char_list));
222 let part: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
223
224 let full_parser = (
227 first_char,
230 parts,
233 part,
236 peek('-')
239 .context(StrContext::Label("character in package name"))
240 .context(StrContext::Expected(StrContextValue::Description(
241 "ASCII alphanumeric character",
242 )))
243 .context_with(iter_char_context!(Self::NEVER_FIRST_CHAR)),
244 );
245
246 full_parser.take().map(|n: &str| Name(n.to_owned()))
247 }
248}
249
250impl AlpmParser for Name {
251 fn parser(input: &mut &str) -> ModalResult<Self> {
259 let alphanum = |c: char| c.is_ascii_alphanumeric();
260 let first_char = one_of((alphanum, Self::SPECIAL_FIRST_CHARS))
261 .context(StrContext::Label("first character of package name"))
262 .context(StrContext::Expected(StrContextValue::Description(
263 "ASCII alphanumeric character",
264 )))
265 .context_with(iter_char_context!(Self::SPECIAL_FIRST_CHARS));
266
267 let never_first_char = one_of((alphanum, Self::NEVER_FIRST_CHAR));
268
269 let remaining_chars: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
272
273 let full_parser = (first_char, remaining_chars);
274
275 full_parser
276 .take()
277 .map(|n: &str| Name(n.to_owned()))
278 .parse_next(input)
279 }
280
281 fn delimiter_error_context<'a, O, P>(
282 parser: P,
283 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
284 where
285 P: Parser<&'a str, O, ErrMode<ContextError>>,
286 {
287 parser
288 .context(StrContext::Label("character in package name"))
289 .context(StrContext::Expected(StrContextValue::Description(
290 "ASCII alphanumeric character",
291 )))
292 .context_with(iter_char_context!(Self::NEVER_FIRST_CHAR))
293 }
294}
295
296impl FromStr for Name {
297 type Err = Error;
298
299 fn from_str(s: &str) -> Result<Name, Self::Err> {
307 Ok(Self::parser_until_eof.parse(s)?)
308 }
309}
310
311impl Display for Name {
312 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
313 write!(fmt, "{}", self.inner())
314 }
315}
316
317impl AsRef<str> for Name {
318 fn as_ref(&self) -> &str {
319 self.inner()
320 }
321}
322
323#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
328#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
329pub struct SharedObjectName(pub(crate) String);
330
331impl SharedObjectName {
332 pub fn new(name: &str) -> Result<Self, Error> {
349 Self::from_str(name)
350 }
351
352 pub fn as_str(&self) -> &str {
354 self.0.as_ref()
355 }
356}
357
358impl AlpmParser for SharedObjectName {
359 fn parser(input: &mut &str) -> ModalResult<Self> {
365 let alphanum = |c: char| c.is_ascii_alphanumeric();
369
370 let never_first_char = one_of((alphanum, Name::NEVER_FIRST_CHAR));
371
372 (
373 one_of((alphanum, Name::SPECIAL_FIRST_CHARS))
375 .context(StrContext::Label("first character of name"))
376 .context(StrContext::Expected(StrContextValue::Description(
377 "ASCII alphanumeric character",
378 )))
379 .context_with(iter_char_context!(Name::SPECIAL_FIRST_CHARS)),
380 repeat_till::<_, _, String, _, _, _, _>(1.., never_first_char, peek(alt((".so", eof))))
383 .context(StrContext::Label("name")),
384 repeat::<_, _, String, _, _>(1.., ".so")
386 .take()
387 .context(StrContext::Label("suffix"))
388 .context(StrContext::Expected(StrContextValue::Description(
389 "shared object name suffix '.so'",
390 ))),
391 )
392 .take()
393 .map(|n: &str| SharedObjectName(n.to_owned()))
394 .parse_next(input)
395 }
396
397 fn delimiter_error_context<'a, O, P>(
398 parser: P,
399 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
400 where
401 P: Parser<&'a str, O, ErrMode<ContextError>>,
402 {
403 parser
404 .context(StrContext::Label("shared object name"))
405 .context(StrContext::Expected(StrContextValue::Description(
406 "end of input.",
407 )))
408 }
409}
410
411impl FromStr for SharedObjectName {
412 type Err = Error;
413 fn from_str(s: &str) -> Result<Self, Self::Err> {
415 Ok(Self::parser_until_eof.parse(s)?)
416 }
417}
418
419impl Display for SharedObjectName {
420 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
421 write!(fmt, "{}", self.0)
422 }
423}
424
425#[cfg(test)]
426mod tests {
427 use insta::assert_snapshot;
428 use proptest::prelude::*;
429 use rstest::rstest;
430
431 use super::*;
432 use crate::configure_insta;
433
434 #[rstest]
435 #[case(
436 "bar",
437 ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(),
438 Ok(BuildTool::from_str("bar").unwrap()),
439 )]
440 #[case(
441 "bar",
442 ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(),
443 Err(Error::ValueDoesNotMatchRestrictions {
444 restrictions: vec!["foo".to_string(), "foo".to_string()],
445 }),
446 )]
447 fn buildtool_new_with_restriction(
448 #[case] buildtool: &str,
449 #[case] restrictions: Vec<Name>,
450 #[case] result: Result<BuildTool, Error>,
451 ) {
452 assert_eq!(
453 BuildTool::new_with_restriction(buildtool, &restrictions),
454 result
455 );
456 }
457
458 #[rstest]
459 #[case("bar", ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(), true)]
460 #[case("bar", ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(), false)]
461 fn buildtool_matches_restriction(
462 #[case] buildtool: &str,
463 #[case] restrictions: Vec<Name>,
464 #[case] result: bool,
465 ) {
466 let buildtool = BuildTool::from_str(buildtool).unwrap();
467 assert_eq!(buildtool.matches_restriction(&restrictions), result);
468 }
469
470 #[rstest]
471 #[case("package_name_'''")]
472 #[case("-package_with_leading_hyphen")]
473 fn name_parse_error(#[case] input: &str) {
474 let Err(Error::ParseError(err_msg)) = Name::from_str(input) else {
475 panic!("'{input}' erroneously parsed as a Name")
476 };
477
478 let (test_name, _guard) = configure_insta();
479 assert_snapshot!(test_name, err_msg.to_string());
480 }
481
482 proptest! {
483 #![proptest_config(ProptestConfig::with_cases(1000))]
484
485 #[test]
486 fn valid_name_from_string(name_str in r"[a-zA-Z0-9_@+]+[a-zA-Z0-9\-._@+]*") {
487 let name = Name::from_str(&name_str).unwrap();
488 prop_assert_eq!(name_str, format!("{}", name));
489 }
490
491 #[test]
492 fn invalid_name_from_string_start(name_str in r"[-.][a-zA-Z0-9@._+-]*") {
493 let error = Name::from_str(&name_str).unwrap_err();
494 assert!(matches!(error, Error::ParseError(_)));
495 }
496
497 #[test]
498 fn invalid_name_with_invalid_characters(name_str in r"[^\w@._+-]+") {
499 let error = Name::from_str(&name_str).unwrap_err();
500 assert!(matches!(error, Error::ParseError(_)));
501 }
502 }
503
504 #[rstest]
505 #[case("example.so", SharedObjectName("example.so".parse().unwrap()))]
506 #[case("example.so.so", SharedObjectName("example.so.so".parse().unwrap()))]
507 #[case("libexample.1.so", SharedObjectName("libexample.1.so".parse().unwrap()))]
508 fn shared_object_name_parser(
509 #[case] input: &str,
510 #[case] expected_result: SharedObjectName,
511 ) -> testresult::TestResult<()> {
512 let shared_object_name = SharedObjectName::new(input)?;
513 assert_eq!(expected_result, shared_object_name);
514 assert_eq!(input, shared_object_name.as_str());
515 Ok(())
516 }
517
518 #[rstest]
519 #[case("noso")]
520 #[case("example.so.1")]
521 fn invalid_shared_object_name_parser(#[case] input: &str) {
522 let Err(Error::ParseError(err_msg)) = SharedObjectName::from_str(input) else {
523 panic!("'{input}' erroneously parsed as a SonameV2")
524 };
525
526 let (test_name, _guard) = configure_insta();
527 assert_snapshot!(test_name, err_msg.to_string());
528 }
529}