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, 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, Eq, PartialEq, Serialize, Deserialize)]
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 proptest::prelude::*;
306 use rstest::rstest;
307
308 use super::*;
309
310 #[rstest]
311 #[case(
312 "bar",
313 ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(),
314 Ok(BuildTool::from_str("bar").unwrap()),
315 )]
316 #[case(
317 "bar",
318 ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(),
319 Err(Error::ValueDoesNotMatchRestrictions {
320 restrictions: vec!["foo".to_string(), "foo".to_string()],
321 }),
322 )]
323 fn buildtool_new_with_restriction(
324 #[case] buildtool: &str,
325 #[case] restrictions: Vec<Name>,
326 #[case] result: Result<BuildTool, Error>,
327 ) {
328 assert_eq!(
329 BuildTool::new_with_restriction(buildtool, &restrictions),
330 result
331 );
332 }
333
334 #[rstest]
335 #[case("bar", ["foo".parse(), "bar".parse()].into_iter().flatten().collect::<Vec<Name>>(), true)]
336 #[case("bar", ["foo".parse(), "foo".parse()].into_iter().flatten().collect::<Vec<Name>>(), false)]
337 fn buildtool_matches_restriction(
338 #[case] buildtool: &str,
339 #[case] restrictions: Vec<Name>,
340 #[case] result: bool,
341 ) {
342 let buildtool = BuildTool::from_str(buildtool).unwrap();
343 assert_eq!(buildtool.matches_restriction(&restrictions), result);
344 }
345
346 #[rstest]
347 #[case("package_name_'''", "invalid character in package name")]
348 #[case("-package_with_leading_hyphen", "invalid first character")]
349 fn name_parse_error(#[case] input: &str, #[case] err_snippet: &str) {
350 let Err(Error::ParseError(err_msg)) = Name::from_str(input) else {
351 panic!("'{input}' erroneously parsed as a Name")
352 };
353 assert!(
354 err_msg.contains(err_snippet),
355 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
356 );
357 }
358
359 proptest! {
360 #![proptest_config(ProptestConfig::with_cases(1000))]
361
362 #[test]
363 fn valid_name_from_string(name_str in r"[a-zA-Z0-9_@+]+[a-zA-Z0-9\-._@+]*") {
364 let name = Name::from_str(&name_str).unwrap();
365 prop_assert_eq!(name_str, format!("{}", name));
366 }
367
368 #[test]
369 fn invalid_name_from_string_start(name_str in r"[-.][a-zA-Z0-9@._+-]*") {
370 let error = Name::from_str(&name_str).unwrap_err();
371 assert!(matches!(error, Error::ParseError(_)));
372 }
373
374 #[test]
375 fn invalid_name_with_invalid_characters(name_str in r"[^\w@._+-]+") {
376 let error = Name::from_str(&name_str).unwrap_err();
377 assert!(matches!(error, Error::ParseError(_)));
378 }
379 }
380
381 #[rstest]
382 #[case("example.so", SharedObjectName("example.so".parse().unwrap()))]
383 #[case("example.so.so", SharedObjectName("example.so.so".parse().unwrap()))]
384 #[case("libexample.1.so", SharedObjectName("libexample.1.so".parse().unwrap()))]
385 fn shared_object_name_parser(
386 #[case] input: &str,
387 #[case] expected_result: SharedObjectName,
388 ) -> testresult::TestResult<()> {
389 let shared_object_name = SharedObjectName::new(input)?;
390 assert_eq!(expected_result, shared_object_name);
391 assert_eq!(input, shared_object_name.as_str());
392 Ok(())
393 }
394
395 #[rstest]
396 #[case("noso", "expected shared object name suffix '.so'")]
397 #[case("example.so.1", "unexpected trailing content after shared object name")]
398 fn invalid_shared_object_name_parser(#[case] input: &str, #[case] error_snippet: &str) {
399 let result = SharedObjectName::from_str(input);
400 assert!(result.is_err(), "Expected SharedObjectName parsing to fail");
401 let err = result.unwrap_err();
402 let pretty_error = err.to_string();
403 assert!(
404 pretty_error.contains(error_snippet),
405 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
406 );
407 }
408}