1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4 string::ToString,
5};
6
7use alpm_parsers::iter_char_context;
8use serde::{Deserialize, Serialize};
9use winnow::{
10 ModalResult,
11 Parser,
12 combinator::{Repeat, alt, cut_err, eof, peek, repeat, repeat_till},
13 error::{StrContext, StrContextValue},
14 stream::Stream,
15 token::{any, one_of, rest},
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.inner()))
84 }
85
86 pub fn inner(&self) -> &Name {
88 &self.0
89 }
90}
91
92impl FromStr for BuildTool {
93 type Err = Error;
94 fn from_str(s: &str) -> Result<BuildTool, Self::Err> {
96 Name::new(s).map(BuildTool)
97 }
98}
99
100impl Display for BuildTool {
101 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
102 write!(fmt, "{}", self.inner())
103 }
104}
105
106#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
133pub struct Name(String);
134
135impl Name {
136 pub fn new(name: &str) -> Result<Self, Error> {
138 Self::from_str(name)
139 }
140
141 pub fn inner(&self) -> &str {
143 &self.0
144 }
145
146 pub fn parser(input: &mut &str) -> ModalResult<Self> {
154 let alphanum = |c: char| c.is_ascii_alphanumeric();
155 let special_first_chars = ['_', '@', '+'];
156 let first_char = one_of((alphanum, special_first_chars))
157 .context(StrContext::Label("first character of package name"))
158 .context(StrContext::Expected(StrContextValue::Description(
159 "ASCII alphanumeric character",
160 )))
161 .context_with(iter_char_context!(special_first_chars));
162
163 let never_first_special_chars = ['_', '@', '+', '-', '.'];
164 let never_first_char = one_of((alphanum, never_first_special_chars));
165
166 let remaining_chars: Repeat<_, _, _, (), _> = repeat(0.., never_first_char);
169
170 let full_parser = (
171 first_char,
172 remaining_chars,
173 eof.context(StrContext::Label("character in package name"))
175 .context(StrContext::Expected(StrContextValue::Description(
176 "ASCII alphanumeric character",
177 )))
178 .context_with(iter_char_context!(never_first_special_chars)),
179 );
180
181 full_parser
182 .take()
183 .map(|n: &str| Name(n.to_owned()))
184 .parse_next(input)
185 }
186}
187
188impl FromStr for Name {
189 type Err = Error;
190
191 fn from_str(s: &str) -> Result<Name, Self::Err> {
199 Ok(Self::parser.parse(s)?)
200 }
201}
202
203impl Display for Name {
204 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
205 write!(fmt, "{}", self.inner())
206 }
207}
208
209impl AsRef<str> for Name {
210 fn as_ref(&self) -> &str {
211 self.inner()
212 }
213}
214
215#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
220pub struct SharedObjectName(pub(crate) Name);
221
222impl SharedObjectName {
223 pub fn new(name: &str) -> Result<Self, Error> {
240 Self::from_str(name)
241 }
242
243 pub fn as_str(&self) -> &str {
245 self.0.as_ref()
246 }
247
248 pub fn parser(input: &mut &str) -> ModalResult<Self> {
250 let checkpoint = input.checkpoint();
255
256 repeat_till::<_, _, String, _, _, _, _>(1.., any, peek(alt((".so", eof))))
258 .context(StrContext::Label("name"))
259 .parse_next(input)?;
260
261 cut_err(repeat::<_, _, String, _, _>(1.., ".so").take())
263 .context(StrContext::Label("suffix"))
264 .context(StrContext::Expected(StrContextValue::Description(
265 "shared object name suffix '.so'",
266 )))
267 .parse_next(input)?;
268
269 cut_err(eof)
271 .context(StrContext::Label(
272 "unexpected trailing content after shared object name.",
273 ))
274 .context(StrContext::Expected(StrContextValue::Description(
275 "end of input.",
276 )))
277 .parse_next(input)?;
278
279 input.reset(&checkpoint);
280 let name = rest
281 .and_then(Name::parser)
282 .context(StrContext::Label("name"))
283 .parse_next(input)?;
284
285 Ok(SharedObjectName(name))
286 }
287}
288
289impl FromStr for SharedObjectName {
290 type Err = Error;
291 fn from_str(s: &str) -> Result<Self, Self::Err> {
293 Ok(Self::parser.parse(s)?)
294 }
295}
296
297impl Display for SharedObjectName {
298 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
299 write!(fmt, "{}", self.0)
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use insta::assert_snapshot;
306 use proptest::prelude::*;
307 use rstest::rstest;
308
309 use super::*;
310 use crate::configure_insta;
311
312 #[rstest]
313 #[case(
314 "bar",
315 ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(),
316 Ok(BuildTool::from_str("bar").unwrap()),
317 )]
318 #[case(
319 "bar",
320 ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(),
321 Err(Error::ValueDoesNotMatchRestrictions {
322 restrictions: vec!["foo".to_string(), "foo".to_string()],
323 }),
324 )]
325 fn buildtool_new_with_restriction(
326 #[case] buildtool: &str,
327 #[case] restrictions: Vec<Name>,
328 #[case] result: Result<BuildTool, Error>,
329 ) {
330 assert_eq!(
331 BuildTool::new_with_restriction(buildtool, &restrictions),
332 result
333 );
334 }
335
336 #[rstest]
337 #[case("bar", ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(), true)]
338 #[case("bar", ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(), false)]
339 fn buildtool_matches_restriction(
340 #[case] buildtool: &str,
341 #[case] restrictions: Vec<Name>,
342 #[case] result: bool,
343 ) {
344 let buildtool = BuildTool::from_str(buildtool).unwrap();
345 assert_eq!(buildtool.matches_restriction(&restrictions), result);
346 }
347
348 #[rstest]
349 #[case("package_name_'''")]
350 #[case("-package_with_leading_hyphen")]
351 fn name_parse_error(#[case] input: &str) {
352 let Err(Error::ParseError(err_msg)) = Name::from_str(input) else {
353 panic!("'{input}' erroneously parsed as a Name")
354 };
355
356 let (test_name, _guard) = configure_insta();
357 assert_snapshot!(test_name, err_msg.to_string());
358 }
359
360 proptest! {
361 #![proptest_config(ProptestConfig::with_cases(1000))]
362
363 #[test]
364 fn valid_name_from_string(name_str in r"[a-zA-Z0-9_@+]+[a-zA-Z0-9\-._@+]*") {
365 let name = Name::from_str(&name_str).unwrap();
366 prop_assert_eq!(name_str, format!("{}", name));
367 }
368
369 #[test]
370 fn invalid_name_from_string_start(name_str in r"[-.][a-zA-Z0-9@._+-]*") {
371 let error = Name::from_str(&name_str).unwrap_err();
372 assert!(matches!(error, Error::ParseError(_)));
373 }
374
375 #[test]
376 fn invalid_name_with_invalid_characters(name_str in r"[^\w@._+-]+") {
377 let error = Name::from_str(&name_str).unwrap_err();
378 assert!(matches!(error, Error::ParseError(_)));
379 }
380 }
381
382 #[rstest]
383 #[case("example.so", SharedObjectName("example.so".parse().unwrap()))]
384 #[case("example.so.so", SharedObjectName("example.so.so".parse().unwrap()))]
385 #[case("libexample.1.so", SharedObjectName("libexample.1.so".parse().unwrap()))]
386 fn shared_object_name_parser(
387 #[case] input: &str,
388 #[case] expected_result: SharedObjectName,
389 ) -> testresult::TestResult<()> {
390 let shared_object_name = SharedObjectName::new(input)?;
391 assert_eq!(expected_result, shared_object_name);
392 assert_eq!(input, shared_object_name.as_str());
393 Ok(())
394 }
395
396 #[rstest]
397 #[case("noso")]
398 #[case("example.so.1")]
399 fn invalid_shared_object_name_parser(#[case] input: &str) {
400 let Err(Error::ParseError(err_msg)) = SharedObjectName::from_str(input) else {
401 panic!("'{input}' erroneously parsed as a SonameV2")
402 };
403
404 let (test_name, _guard) = configure_insta();
405 assert_snapshot!(test_name, err_msg.to_string());
406 }
407}