1use path_slash::PathBufExt;
2use reqwest::Url;
3use serde::{de, Deserialize, Deserializer};
4use std::{convert::Infallible, fs, io, ops::Deref, path::PathBuf, str::FromStr};
5use thiserror::Error;
6
7use crate::{
8 git::{
9 url::{RemoteGitUrl, RemoteGitUrlParseError},
10 GitSource,
11 },
12 lua_rockspec::per_platform_from_intermediate,
13};
14
15use super::{
16 DisplayAsLuaKV, DisplayLuaKV, DisplayLuaValue, PartialOverride, PerPlatform,
17 PlatformOverridable,
18};
19
20#[derive(Default, Deserialize, Clone, Debug, PartialEq)]
21pub struct LocalRockSource {
22 pub archive_name: Option<PathBuf>,
23 pub unpack_dir: Option<PathBuf>,
24}
25
26#[derive(Deserialize, Clone, Debug, PartialEq)]
27pub struct RemoteRockSource {
28 pub(crate) local: LocalRockSource,
29 pub source_spec: RockSourceSpec,
30}
31
32impl From<RockSourceSpec> for RemoteRockSource {
33 fn from(source_spec: RockSourceSpec) -> Self {
34 Self {
35 local: LocalRockSource::default(),
36 source_spec,
37 }
38 }
39}
40
41impl Deref for RemoteRockSource {
42 type Target = LocalRockSource;
43
44 fn deref(&self) -> &Self::Target {
45 &self.local
46 }
47}
48
49#[derive(Error, Debug)]
50pub enum RockSourceError {
51 #[error("invalid rockspec source field combination")]
52 InvalidCombination,
53 #[error(transparent)]
54 SourceUrl(#[from] SourceUrlError),
55 #[error("source URL missing")]
56 SourceUrlMissing,
57}
58
59impl From<RockSourceInternal> for LocalRockSource {
60 fn from(internal: RockSourceInternal) -> Self {
61 LocalRockSource {
62 archive_name: internal.file,
63 unpack_dir: internal.dir,
64 }
65 }
66}
67
68impl TryFrom<RockSourceInternal> for RemoteRockSource {
69 type Error = RockSourceError;
70
71 fn try_from(internal: RockSourceInternal) -> Result<Self, Self::Error> {
72 let local = LocalRockSource::from(internal.clone());
73
74 let url = SourceUrl::from_str(&internal.url.ok_or(RockSourceError::SourceUrlMissing)?)?;
77
78 let source_spec = match (url, internal.tag, internal.branch) {
79 (source, None, None) => Ok(RockSourceSpec::default_from_source_url(source)),
80 (SourceUrl::Git(url), Some(tag), None) => Ok(RockSourceSpec::Git(GitSource {
81 url,
82 checkout_ref: Some(tag),
83 })),
84 (SourceUrl::Git(url), None, Some(branch)) => Ok(RockSourceSpec::Git(GitSource {
85 url,
86 checkout_ref: Some(branch),
87 })),
88 _ => Err(RockSourceError::InvalidCombination),
89 }?;
90
91 Ok(RemoteRockSource { source_spec, local })
92 }
93}
94
95impl<'de> Deserialize<'de> for PerPlatform<RemoteRockSource> {
96 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97 where
98 D: Deserializer<'de>,
99 {
100 per_platform_from_intermediate::<_, RockSourceInternal, _>(deserializer)
101 }
102}
103
104#[derive(Debug, PartialEq, Clone)]
105pub enum RockSourceSpec {
106 Git(GitSource),
107 File(PathBuf),
108 Url(Url),
109}
110
111impl RockSourceSpec {
112 fn default_from_source_url(url: SourceUrl) -> Self {
113 match url {
114 SourceUrl::File(path) => Self::File(path),
115 SourceUrl::Url(url) => Self::Url(url),
116 SourceUrl::Git(url) => Self::Git(GitSource {
117 url,
118 checkout_ref: None,
119 }),
120 }
121 }
122}
123
124impl<'de> Deserialize<'de> for RockSourceSpec {
125 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
126 where
127 D: Deserializer<'de>,
128 {
129 let url = String::deserialize(deserializer)?;
130 Ok(RockSourceSpec::default_from_source_url(
131 url.parse().map_err(de::Error::custom)?,
132 ))
133 }
134}
135
136impl DisplayAsLuaKV for RockSourceSpec {
137 fn display_lua(&self) -> DisplayLuaKV {
138 match self {
139 RockSourceSpec::Git(git_source) => git_source.display_lua(),
140 RockSourceSpec::File(path) => {
141 let mut source_tbl = Vec::new();
142 source_tbl.push(DisplayLuaKV {
143 key: "url".to_string(),
144 value: DisplayLuaValue::String(format!("file:://{}", path.display())),
145 });
146 DisplayLuaKV {
147 key: "source".to_string(),
148 value: DisplayLuaValue::Table(source_tbl),
149 }
150 }
151 RockSourceSpec::Url(url) => {
152 let mut source_tbl = Vec::new();
153 source_tbl.push(DisplayLuaKV {
154 key: "url".to_string(),
155 value: DisplayLuaValue::String(format!("{url}")),
156 });
157 DisplayLuaKV {
158 key: "source".to_string(),
159 value: DisplayLuaValue::Table(source_tbl),
160 }
161 }
162 }
163 }
164}
165
166#[derive(Debug, PartialEq, Deserialize, Clone, Default)]
169pub(crate) struct RockSourceInternal {
170 #[serde(default)]
171 pub(crate) url: Option<String>,
172 pub(crate) file: Option<PathBuf>,
173 pub(crate) dir: Option<PathBuf>,
174 pub(crate) tag: Option<String>,
175 pub(crate) branch: Option<String>,
176}
177
178impl PartialOverride for RockSourceInternal {
179 type Err = Infallible;
180
181 fn apply_overrides(&self, override_spec: &Self) -> Result<Self, Self::Err> {
182 Ok(Self {
183 url: override_opt(override_spec.url.as_ref(), self.url.as_ref()),
184 file: override_opt(override_spec.file.as_ref(), self.file.as_ref()),
185 dir: override_opt(override_spec.dir.as_ref(), self.dir.as_ref()),
186 tag: match &override_spec.branch {
187 None => override_opt(override_spec.tag.as_ref(), self.tag.as_ref()),
188 _ => None,
189 },
190 branch: match &override_spec.tag {
191 None => override_opt(override_spec.branch.as_ref(), self.branch.as_ref()),
192 _ => None,
193 },
194 })
195 }
196}
197
198impl DisplayAsLuaKV for RockSourceInternal {
199 fn display_lua(&self) -> DisplayLuaKV {
200 let mut result = Vec::new();
201
202 if let Some(url) = &self.url {
203 result.push(DisplayLuaKV {
204 key: "url".to_string(),
205 value: DisplayLuaValue::String(url.clone()),
206 });
207 }
208 if let Some(file) = &self.file {
209 result.push(DisplayLuaKV {
210 key: "file".to_string(),
211 value: DisplayLuaValue::String(file.to_slash_lossy().to_string()),
212 });
213 }
214 if let Some(dir) = &self.dir {
215 result.push(DisplayLuaKV {
216 key: "dir".to_string(),
217 value: DisplayLuaValue::String(dir.to_slash_lossy().to_string()),
218 });
219 }
220 if let Some(tag) = &self.tag {
221 result.push(DisplayLuaKV {
222 key: "tag".to_string(),
223 value: DisplayLuaValue::String(tag.clone()),
224 });
225 }
226 if let Some(branch) = &self.branch {
227 result.push(DisplayLuaKV {
228 key: "branch".to_string(),
229 value: DisplayLuaValue::String(branch.clone()),
230 });
231 }
232
233 DisplayLuaKV {
234 key: "source".to_string(),
235 value: DisplayLuaValue::Table(result),
236 }
237 }
238}
239
240#[derive(Error, Debug)]
241#[error("missing source")]
242pub struct RockSourceMissingSource;
243
244impl PlatformOverridable for RockSourceInternal {
245 type Err = RockSourceMissingSource;
246
247 fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
248 where
249 T: PlatformOverridable,
250 {
251 Err(RockSourceMissingSource)
252 }
253}
254
255fn override_opt<T: Clone>(override_opt: Option<&T>, base: Option<&T>) -> Option<T> {
256 override_opt.or(base).cloned()
257}
258
259#[derive(Debug, PartialEq, Clone)]
261pub(crate) enum SourceUrl {
262 File(PathBuf),
264 Url(Url),
266 Git(RemoteGitUrl),
268}
269
270#[derive(Error, Debug)]
271#[error("failed to parse source url: {0}")]
272pub enum SourceUrlError {
273 Io(#[from] io::Error),
274 Git(#[from] RemoteGitUrlParseError),
275 Url(#[source] <Url as FromStr>::Err),
276 #[error("lux does not support rockspecs with CVS sources.")]
277 CVS,
278 #[error("lux does not support rockspecs with mercurial sources.")]
279 Mercurial,
280 #[error("lux does not support rockspecs with SSCM sources.")]
281 SSCM,
282 #[error("lux does not support rockspecs with SVN sources.")]
283 SVN,
284 #[error("unsupported source URL prefix: '{0}+' in URL {1}")]
285 UnsupportedPrefix(String, String),
286 #[error("unsupported source URL: {0}")]
287 Unsupported(String),
288}
289
290impl FromStr for SourceUrl {
291 type Err = SourceUrlError;
292
293 fn from_str(str: &str) -> Result<Self, Self::Err> {
294 match str.split_once("+") {
295 Some(("git" | "gitrec", url)) => Ok(Self::Git(url.parse()?)),
296 Some((prefix, _)) => Err(SourceUrlError::UnsupportedPrefix(
297 prefix.to_string(),
298 str.to_string(),
299 )),
300 None => match str {
301 s if s.starts_with("file://") => {
302 let path_buf: PathBuf = s.trim_start_matches("file://").into();
303 let path = fs::canonicalize(&path_buf)?;
304 Ok(Self::File(path))
305 }
306 s if s.starts_with("git://") => {
307 Ok(Self::Git(s.replacen("git", "https", 1).parse()?))
308 }
309 s if s.ends_with(".git") => Ok(Self::Git(s.parse()?)),
310 s if starts_with_any(s, ["https://", "http://", "ftp://"].into()) => {
311 Ok(Self::Url(s.parse().map_err(SourceUrlError::Url)?))
312 }
313 s if s.starts_with("cvs://") => Err(SourceUrlError::CVS),
314 s if starts_with_any(
315 s,
316 ["hg://", "hg+http://", "hg+https://", "hg+ssh://"].into(),
317 ) =>
318 {
319 Err(SourceUrlError::Mercurial)
320 }
321 s if s.starts_with("sscm://") => Err(SourceUrlError::SSCM),
322 s if s.starts_with("svn://") => Err(SourceUrlError::SVN),
323 s => Err(SourceUrlError::Unsupported(s.to_string())),
324 },
325 }
326 }
327}
328
329impl<'de> Deserialize<'de> for SourceUrl {
330 fn deserialize<D>(deserializer: D) -> Result<SourceUrl, D::Error>
331 where
332 D: Deserializer<'de>,
333 {
334 SourceUrl::from_str(&String::deserialize(deserializer)?).map_err(de::Error::custom)
335 }
336}
337
338fn starts_with_any(str: &str, prefixes: Vec<&str>) -> bool {
339 prefixes.iter().any(|&prefix| str.starts_with(prefix))
340}
341
342#[cfg(test)]
343mod tests {
344
345 use assert_fs::TempDir;
346
347 use super::*;
348
349 #[tokio::test]
350 async fn parse_source_url() {
351 let dir = TempDir::new().unwrap();
352 let url: SourceUrl = format!("file://{}", dir.to_string_lossy()).parse().unwrap();
353 assert_eq!(url, SourceUrl::File(dir.path().to_path_buf()));
354 let url: SourceUrl = "ftp://example.com/foo/bar".parse().unwrap();
355 assert!(matches!(url, SourceUrl::Url { .. }));
356 let url: SourceUrl = "git://example.com/foo/bar".parse().unwrap();
357 assert!(matches!(url, SourceUrl::Git { .. }));
358 SourceUrl::from_str("git+file:///path/to/repo.git").unwrap_err();
360 let url: SourceUrl = "git+http://example.com/foo/bar".parse().unwrap();
361 assert!(matches!(url, SourceUrl::Git { .. }));
362 let url: SourceUrl = "git+https://example.com/foo/bar".parse().unwrap();
363 assert!(matches!(url, SourceUrl::Git { .. }));
364 let url: SourceUrl = "git+ssh://example.com/foo/bar".parse().unwrap();
365 assert!(matches!(url, SourceUrl::Git { .. }));
366 let url: SourceUrl = "gitrec+https://example.com/foo/bar".parse().unwrap();
367 assert!(matches!(url, SourceUrl::Git { .. }));
368 let url: SourceUrl = "https://example.com/foo/bar".parse().unwrap();
369 assert!(matches!(url, SourceUrl::Url { .. }));
370 let url: SourceUrl = "http://example.com/foo/bar".parse().unwrap();
371 assert!(matches!(url, SourceUrl::Url { .. }));
372 }
373}