1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4 string::ToString,
5};
6
7use alpm_parsers::prelude::*;
8#[cfg(feature = "serde")]
9use serde::Serialize;
10#[cfg(feature = "serde")]
11use serde_with::DeserializeFromStr;
12use winnow::{
13 combinator::{Repeat, alt, eof, peek, repeat, repeat_till},
14 error::ErrMode,
15 token::one_of,
16};
17
18use crate::Error;
19
20#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
48pub struct BuildTool(Name);
49
50impl BuildTool {
51 pub fn new(name: Name) -> Self {
53 BuildTool(name)
54 }
55
56 pub fn new_with_restriction(name: &str, restrictions: &[Name]) -> Result<Self, Error> {
69 let buildtool = BuildTool::from_str(name)?;
70 if buildtool.matches_restriction(restrictions) {
71 Ok(buildtool)
72 } else {
73 Err(Error::ValueDoesNotMatchRestrictions {
74 restrictions: restrictions.iter().map(ToString::to_string).collect(),
75 })
76 }
77 }
78
79 pub fn matches_restriction(&self, restrictions: &[Name]) -> bool {
81 restrictions
82 .iter()
83 .any(|restriction| restriction.eq(self.as_ref()))
84 }
85}
86
87impl AsRef<Name> for BuildTool {
88 fn as_ref(&self) -> &Name {
90 &self.0
91 }
92}
93
94impl FromStr for BuildTool {
95 type Err = Error;
96 fn from_str(s: &str) -> Result<BuildTool, Self::Err> {
98 Name::new(s).map(BuildTool)
99 }
100}
101
102impl Display for BuildTool {
103 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
104 write!(fmt, "{}", self.as_ref())
105 }
106}
107
108#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
135#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
136pub struct Name(String);
137
138impl Name {
139 const SPECIAL_FIRST_CHARS: [char; 3] = ['_', '@', '+'];
141 const NEVER_FIRST_CHAR: [char; 5] = ['_', '@', '+', '-', '.'];
143
144 pub fn new(name: &str) -> Result<Self, Error> {
146 Self::from_str(name)
147 }
148}
149
150impl Name {
151 #[expect(clippy::arithmetic_side_effects)]
176 pub(crate) fn parse_name_followed_by_version<'a>(
177 delimiter_count: usize,
178 ) -> impl Parser<Input<'a>, Self, ErrMode<ParseStack<'a>>> {
179 let never_first_char_list = ['_', '@', '+', '.'];
180
181 let alphanum = |c: char| c.is_ascii_alphanumeric();
182 let first_char = one_of((alphanum, Self::SPECIAL_FIRST_CHARS))
183 .label("first character of package name")
184 .expected_text("ASCII alphanumeric character")
185 .expected_chars(Self::SPECIAL_FIRST_CHARS);
186
187 let never_first_char = one_of((alphanum, never_first_char_list));
188
189 let part: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
201 let parts: Repeat<_, _, _, (), _> = repeat(
202 delimiter_count - 1,
203 (
204 part,
205 '-'.label("character in package name")
206 .expected_text("ASCII alphanumeric character")
207 .expected_chars(Self::NEVER_FIRST_CHAR),
208 ),
209 );
210
211 let alphanum = |c: char| c.is_ascii_alphanumeric();
213 let never_first_char = one_of((alphanum, never_first_char_list));
214 let part: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
215
216 (
219 first_char,
222 parts,
225 part,
228 peek('-')
231 .label("character in package name")
232 .expected_text("ASCII alphanumeric character")
233 .expected_chars(Self::NEVER_FIRST_CHAR),
234 )
235 .take()
236 .layer("alpm-package-name")
237 .map(|n: &str| Name(n.to_owned()))
238 }
239}
240
241impl AlpmParser for Name {
242 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
250 let alphanum = |c: char| c.is_ascii_alphanumeric();
251 let first_char = one_of((alphanum, Self::SPECIAL_FIRST_CHARS))
252 .label("first character of package name")
253 .expected_text("ASCII alphanumeric character")
254 .expected_chars(Self::SPECIAL_FIRST_CHARS);
255
256 let never_first_char = one_of((alphanum, Self::NEVER_FIRST_CHAR));
257
258 let remaining_chars: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
261
262 let full_parser = (first_char, remaining_chars);
263
264 full_parser
265 .take()
266 .layer("alpm-package-name")
267 .map(|n: &str| Name(n.to_owned()))
268 .parse_next(input)
269 }
270
271 fn delimiter_error_context<'a, O, P>(
272 parser: P,
273 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
274 where
275 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
276 {
277 parser
278 .label("character in package name")
279 .expected_text("ASCII alphanumeric character")
280 .expected_chars(Self::NEVER_FIRST_CHAR)
281 .layer("alpm-package-name")
282 }
283}
284
285impl FromStr for Name {
286 type Err = Error;
287
288 fn from_str(s: &str) -> Result<Name, Self::Err> {
296 Ok(Self::parser_until_eof.parse(Input::new(s))?)
297 }
298}
299
300impl Display for Name {
301 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
302 write!(fmt, "{}", self.as_ref())
303 }
304}
305
306impl AsRef<str> for Name {
307 fn as_ref(&self) -> &str {
308 &self.0
309 }
310}
311
312#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
317#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
318pub struct SharedObjectName(pub(crate) String);
319
320impl SharedObjectName {
321 pub fn new(name: &str) -> Result<Self, Error> {
338 Self::from_str(name)
339 }
340
341 pub fn as_str(&self) -> &str {
343 self.0.as_ref()
344 }
345}
346
347impl AlpmParser for SharedObjectName {
348 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
354 let alphanum = |c: char| c.is_ascii_alphanumeric();
358
359 let never_first_char = one_of((alphanum, Name::NEVER_FIRST_CHAR));
360
361 (
362 one_of((alphanum, Name::SPECIAL_FIRST_CHARS))
364 .label("first character of name")
365 .expected_text("ASCII alphanumeric character")
366 .expected_chars(Name::SPECIAL_FIRST_CHARS),
367 repeat_till::<_, _, String, _, _, _, _>(1.., never_first_char, peek(alt((".so", eof))))
370 .label("name"),
371 repeat::<_, _, String, _, _>(1.., ".so")
373 .take()
374 .label("suffix")
375 .expected_text("shared object name suffix '.so'"),
376 )
377 .take()
378 .map(|n: &str| SharedObjectName(n.to_owned()))
379 .layer("shared object name")
380 .parse_next(input)
381 }
382
383 fn delimiter_error_context<'a, O, P>(
384 parser: P,
385 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
386 where
387 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
388 {
389 parser
390 .label("shared object name")
391 .expected_text("end of input.")
392 .layer("shared object name")
393 }
394}
395
396impl FromStr for SharedObjectName {
397 type Err = Error;
398 fn from_str(s: &str) -> Result<Self, Self::Err> {
400 Ok(Self::parser_until_eof.parse(Input::new(s))?)
401 }
402}
403
404impl Display for SharedObjectName {
405 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
406 write!(fmt, "{}", self.0)
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use insta::assert_snapshot;
413 use proptest::prelude::*;
414 use rstest::rstest;
415 use testresult::TestResult;
416
417 use super::*;
418 use crate::configure_insta;
419
420 #[test]
422 fn buildtool_new_with_restriction_succeeds() -> TestResult {
423 assert_eq!(
424 BuildTool::new_with_restriction("bar", &["foo".parse()?, "bar".parse()?])?,
425 BuildTool::from_str("bar")?
426 );
427
428 Ok(())
429 }
430
431 #[test]
433 fn buildtool_new_with_restriction_fails_on_unmatched_restriction() -> TestResult {
434 let expected_restrictions = vec!["foo".to_string(), "foo".to_string()];
435 let Err(Error::ValueDoesNotMatchRestrictions {
436 restrictions: error_restrictions,
437 }) = BuildTool::new_with_restriction("bar", &["foo".parse()?, "foo".parse()?])
438 else {
439 panic!("Expected to fail with Error::ValueDoesNotMatchRestrictions");
440 };
441 assert_eq!(expected_restrictions, error_restrictions);
442
443 Ok(())
444 }
445
446 #[rstest]
447 #[case("bar", ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(), true)]
448 #[case("bar", ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(), false)]
449 fn buildtool_matches_restriction(
450 #[case] buildtool: &str,
451 #[case] restrictions: Vec<Name>,
452 #[case] result: bool,
453 ) -> TestResult {
454 let buildtool = BuildTool::from_str(buildtool)?;
455 assert_eq!(buildtool.matches_restriction(&restrictions), result);
456
457 Ok(())
458 }
459
460 #[rstest]
461 #[case("package_name_'''")]
462 #[case("-package_with_leading_hyphen")]
463 fn name_parse_error(#[case] input: &str) {
464 let (test_name, _guard) = configure_insta();
465
466 let Err(Error::ParseError(err_msg)) = Name::from_str(input) else {
467 panic!("'{input}' erroneously parsed as a Name")
468 };
469
470 assert_snapshot!(test_name, err_msg.to_string());
471 }
472
473 #[cfg(feature = "serde")]
475 #[rstest]
476 #[case("package_name_'''")]
477 #[case("-package_with_leading_hyphen")]
478 fn name_deserialize_error(#[case] input: &str) {
479 let Err(serde_json::Error { .. }) = serde_json::from_str::<Name>(&format!("\"{input}\""))
480 else {
481 panic!("'{input}' erroneously deserialized as a Name")
482 };
483 }
484
485 proptest! {
486 #![proptest_config(ProptestConfig::with_cases(1000))]
487
488 #[test]
489 fn valid_name_from_string(name_str in r"[a-zA-Z0-9_@+]+[a-zA-Z0-9\-._@+]*") {
490 let name = Name::from_str(&name_str)?;
491 prop_assert_eq!(name_str, format!("{}", name));
492 }
493
494 #[test]
495 fn invalid_name_from_string_start(name_str in r"[-.][a-zA-Z0-9@._+-]*") {
496 let Err(Error::ParseError(_)) = Name::from_str(&name_str) else {
497 panic!("Expected to return an error, but succeeded instead");
498 };
499 }
500
501 #[test]
502 fn invalid_name_with_invalid_characters(name_str in r"[^\w@._+-]+") {
503 let Err(Error::ParseError(_)) = Name::from_str(&name_str) else {
504 panic!("Expected to return an error, but succeeded instead");
505 };
506 }
507 }
508
509 #[rstest]
510 #[case("example.so", SharedObjectName("example.so".parse()?))]
511 #[case("example.so.so", SharedObjectName("example.so.so".parse()?))]
512 #[case("libexample.1.so", SharedObjectName("libexample.1.so".parse()?))]
513 fn shared_object_name_parser(
514 #[case] input: &str,
515 #[case] expected_result: SharedObjectName,
516 ) -> TestResult {
517 let shared_object_name = SharedObjectName::new(input)?;
518 assert_eq!(expected_result, shared_object_name);
519 assert_eq!(input, shared_object_name.as_str());
520 Ok(())
521 }
522
523 #[rstest]
524 #[case("noso")]
525 #[case("example.so.1")]
526 fn invalid_shared_object_name_parser(#[case] input: &str) {
527 let (test_name, _guard) = configure_insta();
528
529 let Err(Error::ParseError(err_msg)) = SharedObjectName::from_str(input) else {
530 panic!("'{input}' erroneously parsed as a SonameV2")
531 };
532
533 assert_snapshot!(test_name, err_msg.to_string());
534 }
535
536 #[cfg(feature = "serde")]
538 #[rstest]
539 #[case("noso")]
540 #[case("example.so.1")]
541 fn shared_object_deserialize_error(#[case] input: &str) {
542 let Err(serde_json::Error { .. }) =
543 serde_json::from_str::<SharedObjectName>(&format!("\"{input}\""))
544 else {
545 panic!("'{input}' erroneously deserialized as a SharedObjectName")
546 };
547 }
548}