elba/remote/
resolution.rs

1use crate::{
2    package::{Checksum, Name},
3    remote,
4    util::{
5        clear_dir,
6        errors::{ErrorKind, Res},
7        git::{clone, fetch, reset, update_submodules},
8        hexify_hash,
9        lock::DirLock,
10    },
11};
12use failure::{format_err, Error, ResultExt};
13use flate2::read::GzDecoder;
14use git2::{BranchType, Repository, Sort};
15use reqwest::Client;
16use semver::Version;
17use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
18use sha2::{Digest, Sha256};
19use std::{fmt, fs, io::BufReader, path::PathBuf, str::FromStr};
20use tar::Archive;
21use url::Url;
22
23/// The possible places from which a package can be resolved.
24///
25/// There are two main sources from which a package can originate: a Direct source (a path or a
26/// tarball online or a git repo) and an Index (an indirect source which accrues metadata about
27/// Direct sources
28#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29pub enum Resolution {
30    Direct(DirectRes),
31    Index(IndexRes),
32}
33
34impl From<DirectRes> for Resolution {
35    fn from(i: DirectRes) -> Self {
36        Resolution::Direct(i)
37    }
38}
39
40impl From<IndexRes> for Resolution {
41    fn from(i: IndexRes) -> Self {
42        Resolution::Index(i)
43    }
44}
45
46impl FromStr for Resolution {
47    type Err = Error;
48
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        let direct = DirectRes::from_str(s);
51        if direct.is_ok() {
52            direct.map(Resolution::Direct)
53        } else {
54            IndexRes::from_str(s).map(Resolution::Index)
55        }
56    }
57}
58
59impl fmt::Display for Resolution {
60    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61        match self {
62            Resolution::Direct(d) => write!(f, "{}", d),
63            Resolution::Index(i) => write!(f, "{}", i),
64        }
65    }
66}
67
68impl Serialize for Resolution {
69    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
70    where
71        S: Serializer,
72    {
73        serializer.serialize_str(&self.to_string())
74    }
75}
76
77impl<'de> Deserialize<'de> for Resolution {
78    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
79        let s = String::deserialize(deserializer)?;
80        FromStr::from_str(&s).map_err(de::Error::custom)
81    }
82}
83
84impl Resolution {
85    pub fn direct(&self) -> Option<&DirectRes> {
86        if let Resolution::Direct(d) = &self {
87            Some(&d)
88        } else {
89            None
90        }
91    }
92
93    pub fn is_tar(&self) -> bool {
94        if let Resolution::Direct(d) = &self {
95            d.is_tar()
96        } else {
97            false
98        }
99    }
100
101    pub fn is_git(&self) -> bool {
102        if let Resolution::Direct(d) = &self {
103            d.is_git()
104        } else {
105            false
106        }
107    }
108
109    pub fn is_dir(&self) -> bool {
110        if let Resolution::Direct(d) = &self {
111            d.is_dir()
112        } else {
113            false
114        }
115    }
116
117    pub fn lowkey_eq(&self, other: &Resolution) -> bool {
118        match (self, other) {
119            (Resolution::Direct(d), Resolution::Direct(d2)) => d.lowkey_eq(d2),
120            (Resolution::Index(i), Resolution::Index(i2)) => i == i2,
121            (_, _) => false,
122        }
123    }
124}
125
126#[derive(Clone, Debug, PartialEq, Eq, Hash)]
127pub enum DirectRes {
128    /// Git: the package originated from a git repository.
129    Git { repo: Url, tag: String },
130    /// Dir: the package is on disk in a folder directory.
131    Dir { path: PathBuf },
132    /// Tar: the package is an archive stored somewhere.
133    ///
134    /// Tarballs are the only direct resolution which is allowed to have a checksum; this doesn't
135    /// really make sense for DirectRes::Local, and we leave validation of repositories to Git
136    /// itself. Checksums are stored in the fragment of the resolution url, with they key being the
137    /// checksum format.
138    Tar { url: Url, cksum: Option<Checksum> },
139    /// Registry: the package is stored by a registry.
140    Registry {
141        registry: remote::Registry,
142        name: Name,
143        version: Version,
144    },
145}
146
147impl DirectRes {
148    pub fn lowkey_eq(&self, other: &DirectRes) -> bool {
149        match (self, other) {
150            (DirectRes::Git { repo: r1, .. }, DirectRes::Git { repo: r2, .. }) => r1 == r2,
151            _ => self == other,
152        }
153    }
154}
155
156/// Retrieves a package in the form of a tarball.
157fn retrieve_tar(url: Url, client: &Client, target: &DirLock, cksum: Option<&Checksum>) -> Res<()> {
158    client
159        .get(url)
160        .send()
161        .map_err(|_| Error::from(ErrorKind::CannotDownload))
162        .and_then(|mut r| {
163            let mut buf: Vec<u8> = vec![];
164            r.copy_to(&mut buf).context(ErrorKind::CannotDownload)?;
165
166            let hash = hexify_hash(Sha256::digest(&buf[..]).as_slice());
167            if let Some(cksum) = cksum {
168                if cksum.hash != hash {
169                    return Err(format_err!("tarball checksum doesn't match real checksum"))?;
170                }
171            }
172
173            let archive = BufReader::new(&buf[..]);
174            let archive = GzDecoder::new(archive);
175            let mut archive = Archive::new(archive);
176
177            clear_dir(target.path())?;
178
179            archive
180                .unpack(target.path())
181                .context(ErrorKind::CannotDownload)?;
182
183            Ok(())
184        })
185}
186
187impl DirectRes {
188    pub fn retrieve(
189        &self,
190        client: &Client,
191        target: &DirLock,
192        eager: bool,
193        dl_f: impl Fn(bool) -> Res<()>,
194    ) -> Result<Option<DirectRes>, Error> {
195        match self {
196            DirectRes::Tar { url, cksum } => match url.scheme() {
197                "http" | "https" => {
198                    dl_f(true)?;
199                    retrieve_tar(url.clone(), &client, &target, cksum.as_ref())?;
200
201                    Ok(None)
202                }
203                "file" => {
204                    dl_f(false)?;
205                    let mut archive =
206                        fs::File::open(target.path()).context(ErrorKind::CannotDownload)?;
207
208                    let hash = hexify_hash(
209                        Sha256::digest_reader(&mut archive)
210                            .context(ErrorKind::CannotDownload)?
211                            .as_slice(),
212                    );
213
214                    if let Some(cksum) = cksum {
215                        if cksum.hash != hash {
216                            return Err(format_err!(
217                                "tarball checksum doesn't match real checksum"
218                            ))?;
219                        }
220                    }
221
222                    let archive = BufReader::new(archive);
223                    let archive = GzDecoder::new(archive);
224                    let mut archive = Archive::new(archive);
225
226                    clear_dir(target.path())?;
227
228                    archive
229                        .unpack(target.path())
230                        .context(ErrorKind::CannotDownload)?;
231
232                    Ok(None)
233                }
234                _ => unreachable!(),
235            },
236            DirectRes::Git { repo: url, tag } => {
237                // If we find a directory which already has a repo, we just check out the correct
238                // version of it. Whether or not a new dir is created isn't our job, that's for the
239                // Cache. If the Cache points to a directory that already exists, it means that the
240                // branch data or w/e is irrelevant.
241                let repo = Repository::open(target.path());
242                let repo = match repo {
243                    Ok(r) => {
244                        let mut repo = r;
245                        // This logic is for in case we are pointed to an existing git repository.
246                        // We only want to NOT update an existing git repository if eager is false.
247                        // We assume that the HEAD of the repo is at the current "locked" state.
248                        //
249                        // If the tag is a branch:
250                        if !eager {
251                            if let Ok(b) = repo.find_branch(&tag, BranchType::Local) {
252                                let head = b.into_reference().resolve()?.peel_to_commit()?;
253                                let cur = repo.head()?.resolve()?.peel_to_commit()?;
254
255                                let mut revwalk = repo.revwalk()?;
256                                revwalk.push(head.id())?;
257                                revwalk.set_sorting(Sort::TOPOLOGICAL);
258
259                                if revwalk.any(|x| x == Ok(cur.id())) {
260                                    if &cur.id().to_string() == tag {
261                                        return Ok(None);
262                                    } else {
263                                        return Ok(Some(DirectRes::Git {
264                                            repo: url.clone(),
265                                            tag: cur.id().to_string(),
266                                        }));
267                                    }
268                                }
269                            }
270
271                            // Otherwise, if the tag is an exact pointer to a commit, we try to check out to
272                            // it locally without fetching anything
273                            let target =
274                                repo.revparse_single(&tag).and_then(|x| x.peel_to_commit());
275                            let cur = repo
276                                .head()
277                                .and_then(|x| x.resolve())
278                                .and_then(|x| x.peel_to_commit());
279                            if let Ok(t) = target {
280                                if let Ok(c) = cur {
281                                    if t.id() == c.id() {
282                                        if tag == &c.id().to_string() {
283                                            return Ok(None);
284                                        } else {
285                                            return Ok(Some(DirectRes::Git {
286                                                repo: url.clone(),
287                                                tag: c.id().to_string(),
288                                            }));
289                                        }
290                                    } else {
291                                        // Because we know the other tag exists in our local copy of the
292                                        // repo, we can just check out into that and return
293                                        let obj = t.into_object().clone();
294                                        reset(&repo, &obj).with_context(|e| {
295                                            format_err!(
296                                                "couldn't checkout commit {}: {}",
297                                                obj.id(),
298                                                e
299                                            )
300                                        })?;
301                                        if tag == &obj.id().to_string() {
302                                            return Ok(None);
303                                        } else {
304                                            return Ok(Some(DirectRes::Git {
305                                                repo: url.clone(),
306                                                tag: obj.id().to_string(),
307                                            }));
308                                        }
309                                    }
310                                }
311                            }
312                        }
313
314                        // Get everything!!
315                        dl_f(true)?;
316                        let refspec = "refs/heads/*:refs/heads/*";
317                        fetch(&mut repo, &url, refspec).with_context(|e| {
318                            format_err!("couldn't fetch git repo {}: {}", url, e)
319                        })?;
320                        repo
321                    }
322                    Err(_) => {
323                        clear_dir(target.path())?;
324                        dl_f(true)?;
325                        clone(url, target.path()).with_context(|e| {
326                            format_err!("couldn't fetch git repo {}:\n{}", url, e)
327                        })?
328                    }
329                };
330
331                let obj = repo
332                    .revparse_single(&tag)
333                    .context(ErrorKind::CannotDownload)?;
334                reset(&repo, &obj)
335                    .with_context(|e| format_err!("couldn't fetch git repo {}:\n{}", url, e))?;
336                update_submodules(&repo).with_context(|e| {
337                    format_err!("couldn't update submodules for git repo {}:\n{}", url, e)
338                })?;
339
340                let id = obj.peel_to_commit()?.id().to_string();
341
342                Ok(Some(DirectRes::Git {
343                    repo: url.clone(),
344                    tag: id,
345                }))
346            }
347            DirectRes::Dir { path } => {
348                // If this package is located on disk, we don't have to do anything...
349                dl_f(false)?;
350                if path.exists() {
351                    Ok(None)
352                } else {
353                    Err(format_err!("can't find directory {}", path.display()))?
354                }
355            }
356            DirectRes::Registry {
357                registry,
358                name,
359                version,
360            } => {
361                dl_f(true)?;
362                // TODO: Checksums?
363                retrieve_tar(
364                    registry.retrieve_url(&name, &version),
365                    &client,
366                    &target,
367                    None,
368                )?;
369
370                Ok(None)
371            }
372        }
373    }
374
375    pub fn is_tar(&self) -> bool {
376        if let DirectRes::Tar { .. } = &self {
377            true
378        } else {
379            false
380        }
381    }
382
383    pub fn is_git(&self) -> bool {
384        if let DirectRes::Git { .. } = &self {
385            true
386        } else {
387            false
388        }
389    }
390
391    pub fn is_dir(&self) -> bool {
392        if let DirectRes::Dir { .. } = &self {
393            true
394        } else {
395            false
396        }
397    }
398}
399
400impl FromStr for DirectRes {
401    type Err = Error;
402
403    fn from_str(url: &str) -> Result<Self, Self::Err> {
404        let mut parts = url.splitn(2, '+');
405        let utype = parts.next().unwrap();
406        let rest = parts.next().ok_or_else(|| ErrorKind::InvalidSourceUrl)?;
407
408        match utype {
409            "git" => {
410                let mut url = Url::parse(rest).context(ErrorKind::InvalidSourceUrl)?;
411                let tag = url.fragment().unwrap_or_else(|| "master").to_owned();
412
413                url.set_fragment(None);
414                Ok(DirectRes::Git { repo: url, tag })
415            }
416            "dir" => {
417                let path = PathBuf::from(rest);
418                Ok(DirectRes::Dir { path })
419            }
420            "tar" => {
421                let mut url = Url::parse(rest).context(ErrorKind::InvalidSourceUrl)?;
422                if url.scheme() != "http" && url.scheme() != "https" && url.scheme() != "file" {
423                    return Err(ErrorKind::InvalidSourceUrl)?;
424                }
425                let cksum = url.fragment().and_then(|x| Checksum::from_str(x).ok());
426                url.set_fragment(None);
427                Ok(DirectRes::Tar { url, cksum })
428            }
429            "reg" => {
430                let mut url = Url::parse(rest).context(format_err!("invalid registry url"))?;
431                let frag = url
432                    .fragment()
433                    .map(|x| x.to_owned())
434                    .ok_or_else(|| format_err!("registry url missing name/version fragment"))?;
435                url.set_fragment(None);
436
437                let registry = remote::Registry::new(url.clone());
438                let mut name_ver = frag.splitn(2, '|');
439                let name = Name::from_str(name_ver.next().unwrap())?;
440                let version =
441                    Version::from_str(name_ver.next().ok_or_else(|| ErrorKind::InvalidSourceUrl)?)?;
442                Ok(DirectRes::Registry {
443                    registry,
444                    name,
445                    version,
446                })
447            }
448            _ => Err(ErrorKind::InvalidSourceUrl)?,
449        }
450    }
451}
452
453impl fmt::Display for DirectRes {
454    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
455        match self {
456            DirectRes::Git { repo, tag } => write!(f, "git+{}#{}", repo, tag),
457            DirectRes::Dir { path } => write!(f, "dir+{}", path.display()),
458            DirectRes::Tar { url, cksum } => {
459                let url = url.as_str();
460                write!(
461                    f,
462                    "tar+{}{}",
463                    url,
464                    if let Some(cksum) = cksum {
465                        "#".to_string() + &cksum.to_string()
466                    } else {
467                        "".to_string()
468                    },
469                )
470            }
471            DirectRes::Registry {
472                registry,
473                name,
474                version,
475            } => write!(f, "reg+{}#{}|{}", registry, name, version),
476        }
477    }
478}
479
480#[derive(Clone, Debug, PartialEq, Eq, Hash)]
481pub struct IndexRes {
482    pub res: DirectRes,
483}
484
485impl From<DirectRes> for IndexRes {
486    fn from(d: DirectRes) -> Self {
487        IndexRes { res: d }
488    }
489}
490
491impl From<IndexRes> for DirectRes {
492    fn from(i: IndexRes) -> Self {
493        i.res
494    }
495}
496
497impl FromStr for IndexRes {
498    type Err = Error;
499
500    fn from_str(url: &str) -> Result<Self, Self::Err> {
501        let mut parts = url.splitn(2, '+');
502        let utype = parts.next().unwrap();
503        let url = parts.next().ok_or_else(|| ErrorKind::InvalidSourceUrl)?;
504
505        match utype {
506            "index" => {
507                let res = DirectRes::from_str(url).context(ErrorKind::InvalidSourceUrl)?;
508                Ok(IndexRes { res })
509            }
510            _ => Err(ErrorKind::InvalidSourceUrl)?,
511        }
512    }
513}
514
515impl fmt::Display for IndexRes {
516    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
517        let url = self.res.to_string();
518        let mut s = String::with_capacity(url.len() + 10);
519        s.push_str("index+");
520        s.push_str(&url);
521        write!(f, "{}", s)
522    }
523}
524
525impl Serialize for DirectRes {
526    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
527    where
528        S: Serializer,
529    {
530        serializer.serialize_str(&self.to_string())
531    }
532}
533
534impl<'de> Deserialize<'de> for DirectRes {
535    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
536        let s = String::deserialize(deserializer)?;
537        FromStr::from_str(&s).map_err(de::Error::custom)
538    }
539}
540
541impl Serialize for IndexRes {
542    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
543    where
544        S: Serializer,
545    {
546        serializer.serialize_str(&self.to_string())
547    }
548}
549
550impl<'de> Deserialize<'de> for IndexRes {
551    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
552        let s = String::deserialize(deserializer)?;
553        FromStr::from_str(&s).map_err(de::Error::custom)
554    }
555}