1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4 string::ToString,
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{Architecture, Name, Version, error::Error};
10
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
37pub struct MakepkgOption {
38 name: String,
39 on: bool,
40}
41
42impl MakepkgOption {
43 pub fn new(option: &str) -> Result<Self, Error> {
45 Self::from_str(option)
46 }
47
48 pub fn name(&self) -> &str {
50 &self.name
51 }
52
53 pub fn on(&self) -> bool {
55 self.on
56 }
57}
58
59impl FromStr for MakepkgOption {
60 type Err = Error;
61 fn from_str(s: &str) -> Result<MakepkgOption, Self::Err> {
63 let (name, on) = if let Some(name) = s.strip_prefix('!') {
64 (name.to_owned(), false)
65 } else {
66 (s.to_owned(), true)
67 };
68 if let Some(c) = name
69 .chars()
70 .find(|c| !(c.is_alphanumeric() || ['-', '.', '_'].contains(c)))
71 {
72 return Err(Error::ValueContainsInvalidChars { invalid_char: c });
73 }
74 Ok(MakepkgOption { name, on })
75 }
76}
77
78impl Display for MakepkgOption {
79 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
80 write!(fmt, "{}{}", if self.on { "" } else { "!" }, self.name)
81 }
82}
83
84pub type BuildEnvironmentOption = MakepkgOption;
105
106pub type PackageOption = MakepkgOption;
127
128#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
145pub struct InstalledPackage {
146 name: Name,
147 version: Version,
148 architecture: Architecture,
149}
150
151impl InstalledPackage {
152 pub fn new(name: Name, version: Version, architecture: Architecture) -> Result<Self, Error> {
154 Ok(InstalledPackage {
155 name,
156 version,
157 architecture,
158 })
159 }
160}
161
162impl FromStr for InstalledPackage {
163 type Err = Error;
164 fn from_str(s: &str) -> Result<InstalledPackage, Self::Err> {
166 const DELIMITER: char = '-';
167 let mut parts = s.rsplitn(4, DELIMITER);
168
169 let architecture = parts.next().ok_or(Error::MissingComponent {
170 component: "architecture",
171 })?;
172 let architecture = architecture.parse()?;
173 let version = {
174 let Some(pkgrel) = parts.next() else {
175 return Err(Error::MissingComponent {
176 component: "pkgrel",
177 })?;
178 };
179 let Some(epoch_pkgver) = parts.next() else {
180 return Err(Error::MissingComponent {
181 component: "epoch_pkgver",
182 })?;
183 };
184 epoch_pkgver.to_string() + "-" + pkgrel
185 };
186 let name = parts
187 .next()
188 .ok_or(Error::MissingComponent { component: "name" })?
189 .to_string();
190
191 Ok(InstalledPackage {
192 name: Name::new(&name)?,
193 version: Version::with_pkgrel(version.as_str())?,
194 architecture,
195 })
196 }
197}
198
199impl Display for InstalledPackage {
200 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
201 write!(fmt, "{}-{}-{}", self.name, self.version, self.architecture)
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use rstest::rstest;
208
209 use super::*;
210
211 #[rstest]
212 #[case("something", Ok(MakepkgOption{name: "something".to_string(), on: true}))]
213 #[case("1cool.build-option", Ok(MakepkgOption{name: "1cool.build-option".to_string(), on: true}))]
214 #[case("üñıçøĐë", Ok(MakepkgOption{name: "üñıçøĐë".to_string(), on: true}))]
215 #[case("!üñıçøĐë", Ok(MakepkgOption{name: "üñıçøĐë".to_string(), on: false}))]
216 #[case("!something", Ok(MakepkgOption{name: "something".to_string(), on: false}))]
217 #[case("!!something", Err(Error::ValueContainsInvalidChars { invalid_char: '!'}))]
218 #[case("foo\\", Err(Error::ValueContainsInvalidChars { invalid_char: '\\'}))]
219 fn makepkgoption(#[case] s: &str, #[case] result: Result<MakepkgOption, Error>) {
220 assert_eq!(MakepkgOption::from_str(s), result);
221 }
222
223 #[rstest]
224 #[case(
225 "foo-bar-1:1.0.0-1-any",
226 Ok(InstalledPackage{
227 name: Name::new("foo-bar").unwrap(),
228 version: Version::from_str("1:1.0.0-1").unwrap(),
229 architecture: Architecture::Any,
230 }),
231 )]
232 #[case("foo-bar-1:1.0.0-1", Err(strum::ParseError::VariantNotFound.into()))]
233 #[case("1:1.0.0-1-any", Err(Error::MissingComponent { component: "name" }))]
234 fn installed_new(#[case] s: &str, #[case] result: Result<InstalledPackage, Error>) {
235 assert_eq!(InstalledPackage::from_str(s), result);
236 }
237
238 #[rstest]
239 #[case("foo-1:1.0.0-bar-any", "invalid package release")]
240 #[case("packagename-30-0.1oops-any", "expected end of package release value")]
241 #[case("package$with$dollars-30-0.1-any", "invalid character in package name")]
242 fn installed_new_parse_error(#[case] input: &str, #[case] error_snippet: &str) {
243 let result = InstalledPackage::from_str(input);
244 assert!(result.is_err(), "Expected InstalledPackage parsing to fail");
245 let err = result.unwrap_err();
246 let pretty_error = err.to_string();
247 assert!(
248 pretty_error.contains(error_snippet),
249 "Error:\n=====\n{pretty_error}\n=====\nshould contain snippet:\n\n{error_snippet}"
250 );
251 }
252}