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