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