artifactize_core/scope/
argument.rs1use super::{ScopeError, instruction, logical_path};
4use serde::Serialize;
5
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
8pub enum LogicalTarget {
9 Root,
10 Suffix(LogicalSuffix),
11}
12
13#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
15pub struct LogicalSuffix(String);
16
17impl std::str::FromStr for LogicalSuffix {
18 type Err = ScopeError;
19 fn from_str(path: &str) -> Result<Self, Self::Err> {
20 logical_path(path)?;
21 if path.is_empty() {
22 return Err(ScopeError(
23 "Artifact argument suffix must be nonempty.".into(),
24 ));
25 }
26 Ok(Self(path.to_owned()))
27 }
28}
29
30impl std::str::FromStr for LogicalTarget {
31 type Err = ScopeError;
32 fn from_str(path: &str) -> Result<Self, Self::Err> {
33 if path.is_empty() {
34 Ok(Self::Root)
35 } else {
36 path.parse().map(Self::Suffix)
37 }
38 }
39}
40
41impl std::ops::Deref for LogicalTarget {
42 type Target = str;
43 fn deref(&self) -> &str {
44 match self {
45 Self::Root => "",
46 Self::Suffix(path) => &path.0,
47 }
48 }
49}
50
51impl LogicalTarget {
52 pub fn as_str(&self) -> &str {
53 self
54 }
55}
56
57impl Serialize for LogicalTarget {
58 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
59 serializer.serialize_str(self.as_str())
60 }
61}
62
63impl AsRef<std::path::Path> for LogicalTarget {
64 fn as_ref(&self) -> &std::path::Path {
65 std::path::Path::new(self.as_str())
66 }
67}
68
69impl std::fmt::Display for LogicalTarget {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 formatter.write_str(self)
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Argument {
83 Literal(String),
84 Reference {
85 prefix: String,
86 name: crate::config::ReferenceName,
87 path: LogicalTarget,
88 },
89}
90
91impl std::str::FromStr for Argument {
92 type Err = ScopeError;
93 fn from_str(argument: &str) -> Result<Self, Self::Err> {
94 let references = instruction::references(argument);
95 let Some(reference) = references.first() else {
96 return Ok(Self::Literal(argument.to_owned()));
97 };
98 let prefix = &argument[..reference.start];
99 let flag = prefix
100 .strip_prefix("--")
101 .and_then(|flag| flag.strip_suffix('='))
102 .is_some_and(|flag| {
103 !flag.is_empty()
104 && flag
105 .bytes()
106 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
107 });
108 if references.len() != 1 || (!prefix.is_empty() && !flag) {
109 return Err(ScopeError(
110 "Artifact arguments must be {name}[/path] or --flag={name}[/path].".into(),
111 ));
112 }
113 let suffix = &argument[reference.end..];
114 let path = if suffix.is_empty() {
115 ""
116 } else {
117 suffix
118 .strip_prefix('/')
119 .filter(|path| !path.is_empty())
120 .ok_or_else(|| {
121 ScopeError("Artifact argument suffix must be a nonempty /path.".into())
122 })?
123 };
124 logical_path(path)?;
125 Ok(Self::Reference {
126 prefix: prefix.to_owned(),
127 name: reference.name.clone(),
128 path: path.parse()?,
129 })
130 }
131}
132
133impl std::fmt::Display for Argument {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 Self::Literal(text) => f.write_str(text),
137 Self::Reference { prefix, name, path } => {
138 write!(f, "{prefix}{{{name}}}")?;
139 if !path.is_empty() {
140 write!(f, "/{path}")?;
141 }
142 Ok(())
143 }
144 }
145 }
146}
147
148impl TryFrom<String> for Argument {
149 type Error = ScopeError;
150 fn try_from(value: String) -> Result<Self, Self::Error> {
151 value.parse()
152 }
153}
154
155impl From<Argument> for String {
156 fn from(argument: Argument) -> Self {
157 argument.to_string()
158 }
159}
160
161impl Serialize for Argument {
162 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
163 serializer.serialize_str(&self.to_string())
164 }
165}
166
167impl<'de> serde::Deserialize<'de> for Argument {
168 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
169 use serde::de::Error;
170 String::deserialize(deserializer)?
171 .parse()
172 .map_err(|failure: ScopeError| D::Error::custom(failure.0))
173 }
174}
175
176impl From<&str> for Argument {
178 fn from(value: &str) -> Self {
179 value.parse().expect("a valid declared argument")
180 }
181}
182
183impl PartialEq<str> for Argument {
186 fn eq(&self, other: &str) -> bool {
187 other.parse::<Self>().is_ok_and(|parsed| *self == parsed)
188 }
189}
190impl PartialEq<&str> for Argument {
191 fn eq(&self, other: &&str) -> bool {
192 self == *other
193 }
194}
195impl PartialEq<Argument> for str {
196 fn eq(&self, other: &Argument) -> bool {
197 other == self
198 }
199}
200impl PartialEq<Argument> for String {
201 fn eq(&self, other: &Argument) -> bool {
202 other == self.as_str()
203 }
204}
205impl PartialEq<String> for Argument {
206 fn eq(&self, other: &String) -> bool {
207 self == other.as_str()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn reference_suffix_boundary_preserves_root_and_rejects_unsafe_paths() {
217 for text in [
218 "{owner}",
219 "{owner}/folder/file",
220 "--input={owner}/folder/file",
221 ] {
222 let argument: Argument = serde_json::from_value(serde_json::json!(text)).unwrap();
223 assert_eq!(
224 serde_json::to_value(argument).unwrap(),
225 serde_json::json!(text)
226 );
227 }
228 let root: LogicalTarget = "".parse().unwrap();
229 assert_eq!(root, LogicalTarget::Root);
230 assert_eq!(serde_json::to_value(root).unwrap(), "");
231 let suffix: LogicalTarget = "folder/file".parse().unwrap();
232 assert_eq!(serde_json::to_value(suffix).unwrap(), "folder/file");
233 for invalid in ["", ".", "../outside", "a//b", "a/../b", "a\\b", "a\0b"] {
234 assert!(invalid.parse::<LogicalSuffix>().is_err(), "{invalid:?}");
235 }
236 for invalid in [
237 "{owner}/",
238 "{owner}/../outside",
239 "{owner}/a//b",
240 "{owner}/.",
241 ] {
242 assert!(serde_json::from_value::<Argument>(serde_json::json!(invalid)).is_err());
243 }
244 }
245}