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#[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 { repo: Url, tag: String },
130 Dir { path: PathBuf },
132 Tar { url: Url, cksum: Option<Checksum> },
139 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
156fn 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 let repo = Repository::open(target.path());
242 let repo = match repo {
243 Ok(r) => {
244 let mut repo = r;
245 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 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 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 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 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 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}