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::Serialize;
13#[cfg(feature = "serde")]
14use serde_with::DeserializeFromStr;
15use winnow::{
16 ModalResult,
17 Parser,
18 combinator::{Repeat, alt, eof, peek, repeat, repeat_till},
19 error::{ContextError, ErrMode, StrContext, StrContextValue},
20 token::one_of,
21};
22
23use crate::Error;
24
25#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
53pub struct BuildTool(Name);
54
55impl BuildTool {
56 pub fn new(name: Name) -> Self {
58 BuildTool(name)
59 }
60
61 pub fn new_with_restriction(name: &str, restrictions: &[Name]) -> Result<Self, Error> {
74 let buildtool = BuildTool::from_str(name)?;
75 if buildtool.matches_restriction(restrictions) {
76 Ok(buildtool)
77 } else {
78 Err(Error::ValueDoesNotMatchRestrictions {
79 restrictions: restrictions.iter().map(ToString::to_string).collect(),
80 })
81 }
82 }
83
84 pub fn matches_restriction(&self, restrictions: &[Name]) -> bool {
86 restrictions
87 .iter()
88 .any(|restriction| restriction.eq(self.inner()))
89 }
90
91 pub fn inner(&self) -> &Name {
93 &self.0
94 }
95}
96
97impl FromStr for BuildTool {
98 type Err = Error;
99 fn from_str(s: &str) -> Result<BuildTool, Self::Err> {
101 Name::new(s).map(BuildTool)
102 }
103}
104
105impl Display for BuildTool {
106 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
107 write!(fmt, "{}", self.inner())
108 }
109}
110
111#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
138#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
139pub struct Name(String);
140
141impl Name {
142 const SPECIAL_FIRST_CHARS: [char; 3] = ['_', '@', '+'];
144 const NEVER_FIRST_CHAR: [char; 5] = ['_', '@', '+', '-', '.'];
146
147 pub fn new(name: &str) -> Result<Self, Error> {
149 Self::from_str(name)
150 }
151
152 pub fn inner(&self) -> &str {
154 &self.0
155 }
156}
157
158impl Name {
159 pub(crate) fn parse_name_followed_by_version<'a>(
183 delimiter_count: usize,
184 ) -> impl Parser<&'a str, Self, ErrMode<ContextError>> {
185 let never_first_char_list = ['_', '@', '+', '.'];
186
187 let alphanum = |c: char| c.is_ascii_alphanumeric();
188 let first_char = one_of((alphanum, Self::SPECIAL_FIRST_CHARS))
189 .context(StrContext::Label("first character of package name"))
190 .context(StrContext::Expected(StrContextValue::Description(
191 "ASCII alphanumeric character",
192 )))
193 .context_with(iter_char_context!(Self::SPECIAL_FIRST_CHARS));
194
195 let never_first_char = one_of((alphanum, never_first_char_list));
196
197 let part: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
209 let parts: Repeat<_, _, _, (), _> = repeat(
210 delimiter_count - 1,
211 (
212 part,
213 '-'.context(StrContext::Label("character in package name"))
214 .context(StrContext::Expected(StrContextValue::Description(
215 "ASCII alphanumeric character",
216 )))
217 .context_with(iter_char_context!(Self::NEVER_FIRST_CHAR)),
218 ),
219 );
220
221 let alphanum = |c: char| c.is_ascii_alphanumeric();
223 let never_first_char = one_of((alphanum, never_first_char_list));
224 let part: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
225
226 let full_parser = (
229 first_char,
232 parts,
235 part,
238 peek('-')
241 .context(StrContext::Label("character in package name"))
242 .context(StrContext::Expected(StrContextValue::Description(
243 "ASCII alphanumeric character",
244 )))
245 .context_with(iter_char_context!(Self::NEVER_FIRST_CHAR)),
246 );
247
248 full_parser.take().map(|n: &str| Name(n.to_owned()))
249 }
250}
251
252impl AlpmParser for Name {
253 fn parser(input: &mut &str) -> ModalResult<Self> {
261 let alphanum = |c: char| c.is_ascii_alphanumeric();
262 let first_char = one_of((alphanum, Self::SPECIAL_FIRST_CHARS))
263 .context(StrContext::Label("first character of package name"))
264 .context(StrContext::Expected(StrContextValue::Description(
265 "ASCII alphanumeric character",
266 )))
267 .context_with(iter_char_context!(Self::SPECIAL_FIRST_CHARS));
268
269 let never_first_char = one_of((alphanum, Self::NEVER_FIRST_CHAR));
270
271 let remaining_chars: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
274
275 let full_parser = (first_char, remaining_chars);
276
277 full_parser
278 .take()
279 .map(|n: &str| Name(n.to_owned()))
280 .parse_next(input)
281 }
282
283 fn delimiter_error_context<'a, O, P>(
284 parser: P,
285 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
286 where
287 P: Parser<&'a str, O, ErrMode<ContextError>>,
288 {
289 parser
290 .context(StrContext::Label("character in package name"))
291 .context(StrContext::Expected(StrContextValue::Description(
292 "ASCII alphanumeric character",
293 )))
294 .context_with(iter_char_context!(Self::NEVER_FIRST_CHAR))
295 }
296}
297
298impl FromStr for Name {
299 type Err = Error;
300
301 fn from_str(s: &str) -> Result<Name, Self::Err> {
309 Ok(Self::parser_until_eof.parse(s)?)
310 }
311}
312
313impl Display for Name {
314 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
315 write!(fmt, "{}", self.inner())
316 }
317}
318
319impl AsRef<str> for Name {
320 fn as_ref(&self) -> &str {
321 self.inner()
322 }
323}
324
325#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
330#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
331pub struct SharedObjectName(pub(crate) String);
332
333impl SharedObjectName {
334 pub fn new(name: &str) -> Result<Self, Error> {
351 Self::from_str(name)
352 }
353
354 pub fn as_str(&self) -> &str {
356 self.0.as_ref()
357 }
358}
359
360impl AlpmParser for SharedObjectName {
361 fn parser(input: &mut &str) -> ModalResult<Self> {
367 let alphanum = |c: char| c.is_ascii_alphanumeric();
371
372 let never_first_char = one_of((alphanum, Name::NEVER_FIRST_CHAR));
373
374 (
375 one_of((alphanum, Name::SPECIAL_FIRST_CHARS))
377 .context(StrContext::Label("first character of name"))
378 .context(StrContext::Expected(StrContextValue::Description(
379 "ASCII alphanumeric character",
380 )))
381 .context_with(iter_char_context!(Name::SPECIAL_FIRST_CHARS)),
382 repeat_till::<_, _, String, _, _, _, _>(1.., never_first_char, peek(alt((".so", eof))))
385 .context(StrContext::Label("name")),
386 repeat::<_, _, String, _, _>(1.., ".so")
388 .take()
389 .context(StrContext::Label("suffix"))
390 .context(StrContext::Expected(StrContextValue::Description(
391 "shared object name suffix '.so'",
392 ))),
393 )
394 .take()
395 .map(|n: &str| SharedObjectName(n.to_owned()))
396 .parse_next(input)
397 }
398
399 fn delimiter_error_context<'a, O, P>(
400 parser: P,
401 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
402 where
403 P: Parser<&'a str, O, ErrMode<ContextError>>,
404 {
405 parser
406 .context(StrContext::Label("shared object name"))
407 .context(StrContext::Expected(StrContextValue::Description(
408 "end of input.",
409 )))
410 }
411}
412
413impl FromStr for SharedObjectName {
414 type Err = Error;
415 fn from_str(s: &str) -> Result<Self, Self::Err> {
417 Ok(Self::parser_until_eof.parse(s)?)
418 }
419}
420
421impl Display for SharedObjectName {
422 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
423 write!(fmt, "{}", self.0)
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use insta::assert_snapshot;
430 use proptest::prelude::*;
431 use rstest::rstest;
432
433 use super::*;
434 use crate::configure_insta;
435
436 #[rstest]
437 #[case(
438 "bar",
439 ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(),
440 Ok(BuildTool::from_str("bar").unwrap()),
441 )]
442 #[case(
443 "bar",
444 ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(),
445 Err(Error::ValueDoesNotMatchRestrictions {
446 restrictions: vec!["foo".to_string(), "foo".to_string()],
447 }),
448 )]
449 fn buildtool_new_with_restriction(
450 #[case] buildtool: &str,
451 #[case] restrictions: Vec<Name>,
452 #[case] result: Result<BuildTool, Error>,
453 ) {
454 assert_eq!(
455 BuildTool::new_with_restriction(buildtool, &restrictions),
456 result
457 );
458 }
459
460 #[rstest]
461 #[case("bar", ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(), true)]
462 #[case("bar", ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(), false)]
463 fn buildtool_matches_restriction(
464 #[case] buildtool: &str,
465 #[case] restrictions: Vec<Name>,
466 #[case] result: bool,
467 ) {
468 let buildtool = BuildTool::from_str(buildtool).unwrap();
469 assert_eq!(buildtool.matches_restriction(&restrictions), result);
470 }
471
472 #[rstest]
473 #[case("package_name_'''")]
474 #[case("-package_with_leading_hyphen")]
475 fn name_parse_error(#[case] input: &str) {
476 let Err(Error::ParseError(err_msg)) = Name::from_str(input) else {
477 panic!("'{input}' erroneously parsed as a Name")
478 };
479
480 let (test_name, _guard) = configure_insta();
481 assert_snapshot!(test_name, err_msg.to_string());
482 }
483
484 #[cfg(feature = "serde")]
486 #[rstest]
487 #[case("package_name_'''")]
488 #[case("-package_with_leading_hyphen")]
489 fn name_deserialize_error(#[case] input: &str) {
490 let Err(serde_json::Error { .. }) = serde_json::from_str::<Name>(&format!("\"{input}\""))
491 else {
492 panic!("'{input}' erroneously deserialized as a Name")
493 };
494 }
495
496 proptest! {
497 #![proptest_config(ProptestConfig::with_cases(1000))]
498
499 #[test]
500 fn valid_name_from_string(name_str in r"[a-zA-Z0-9_@+]+[a-zA-Z0-9\-._@+]*") {
501 let name = Name::from_str(&name_str).unwrap();
502 prop_assert_eq!(name_str, format!("{}", name));
503 }
504
505 #[test]
506 fn invalid_name_from_string_start(name_str in r"[-.][a-zA-Z0-9@._+-]*") {
507 let error = Name::from_str(&name_str).unwrap_err();
508 assert!(matches!(error, Error::ParseError(_)));
509 }
510
511 #[test]
512 fn invalid_name_with_invalid_characters(name_str in r"[^\w@._+-]+") {
513 let error = Name::from_str(&name_str).unwrap_err();
514 assert!(matches!(error, Error::ParseError(_)));
515 }
516 }
517
518 #[rstest]
519 #[case("example.so", SharedObjectName("example.so".parse().unwrap()))]
520 #[case("example.so.so", SharedObjectName("example.so.so".parse().unwrap()))]
521 #[case("libexample.1.so", SharedObjectName("libexample.1.so".parse().unwrap()))]
522 fn shared_object_name_parser(
523 #[case] input: &str,
524 #[case] expected_result: SharedObjectName,
525 ) -> testresult::TestResult<()> {
526 let shared_object_name = SharedObjectName::new(input)?;
527 assert_eq!(expected_result, shared_object_name);
528 assert_eq!(input, shared_object_name.as_str());
529 Ok(())
530 }
531
532 #[rstest]
533 #[case("noso")]
534 #[case("example.so.1")]
535 fn invalid_shared_object_name_parser(#[case] input: &str) {
536 let Err(Error::ParseError(err_msg)) = SharedObjectName::from_str(input) else {
537 panic!("'{input}' erroneously parsed as a SonameV2")
538 };
539
540 let (test_name, _guard) = configure_insta();
541 assert_snapshot!(test_name, err_msg.to_string());
542 }
543
544 #[cfg(feature = "serde")]
546 #[rstest]
547 #[case("noso")]
548 #[case("example.so.1")]
549 fn shared_object_deserialize_error(#[case] input: &str) {
550 let Err(serde_json::Error { .. }) =
551 serde_json::from_str::<SharedObjectName>(&format!("\"{input}\""))
552 else {
553 panic!("'{input}' erroneously deserialized as a SharedObjectName")
554 };
555 }
556}