From ef6a3d0ef7a47a23cdafbd6fd36f8c1faad61b2e Mon Sep 17 00:00:00 2001
From: ryan <ryan@kurte.nz>
Date: Thu, 31 Dec 2020 15:32:58 +1300
Subject: [PATCH] fix version matching, now works with semver

---
 Cargo.lock     | 127 +++++++++++++++++++++++++++++++++++++++++++++++++
 Cargo.toml     |  10 +---
 src/drivers.rs |  95 ++++++++++++++++++++++++++----------
 src/main.rs    |  23 +++++----
 4 files changed, 210 insertions(+), 45 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 58b6a742..a93ae818 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -134,12 +134,14 @@ dependencies = [
  "anyhow",
  "cargo_metadata",
  "cargo_toml",
+ "crates-index",
  "crates_io_api",
  "dirs",
  "env_logger",
  "flate2",
  "log",
  "reqwest",
+ "semver",
  "serde",
  "serde_derive",
  "simplelog",
@@ -180,6 +182,9 @@ name = "cc"
 version = "1.0.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
+dependencies = [
+ "jobserver",
+]
 
 [[package]]
 name = "cfg-if"
@@ -244,6 +249,24 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
 
+[[package]]
+name = "crates-index"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24823d553339d125040d989d2a593a01b034fe5ac17714423bcd2c3d168878"
+dependencies = [
+ "git2",
+ "glob",
+ "hex",
+ "home",
+ "memchr",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smartstring",
+]
+
 [[package]]
 name = "crates_io_api"
 version = "0.6.1"
@@ -530,6 +553,27 @@ dependencies = [
  "wasi 0.9.0+wasi-snapshot-preview1",
 ]
 
+[[package]]
+name = "git2"
+version = "0.13.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f267c9da8a4de3c615b59e23606c75f164f84896e97f4dd6c15a4294de4359"
+dependencies = [
+ "bitflags",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
 [[package]]
 name = "h2"
 version = "0.2.7"
@@ -574,6 +618,24 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hex"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "home"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "http"
 version = "0.2.2"
@@ -692,6 +754,15 @@ version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
+[[package]]
+name = "jobserver"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "js-sys"
 version = "0.3.46"
@@ -723,6 +794,46 @@ version = "0.2.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
 
+[[package]]
+name = "libgit2-sys"
+version = "0.12.17+1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ebdf65ca745126df8824688637aa0535a88900b83362d8ca63893bcf4e8841"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "libssh2-sys"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df40b13fe7ea1be9b9dffa365a51273816c345fc1811478b57ed7d964fbfc4ce"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "log"
 version = "0.4.11"
@@ -1384,6 +1495,16 @@ version = "0.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
 
+[[package]]
+name = "smartstring"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ada87540bf8ef4cf8a1789deb175626829bb59b1fefd816cf7f7f55efcdbae9"
+dependencies = [
+ "serde",
+ "static_assertions",
+]
+
 [[package]]
 name = "socket2"
 version = "0.3.19"
@@ -1395,6 +1516,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
 [[package]]
 name = "strsim"
 version = "0.8.0"
diff --git a/Cargo.toml b/Cargo.toml
index 0e9fe478..571099d2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,10 +7,6 @@ authors = ["ryan <ryan@kurte.nz>"]
 edition = "2018"
 license = "GPL-3.0"
 
-[package.metadata.binstall]
-pkg-url = "https://github.com/ryankurte/cargo-binstall/releases/download/v{ version }/cargo-binstall-{ target }.tgz"
-pkg-fmt = "tgz"
-
 [[pkg_bin]]
 name = "cargo-binstall"
 path = "cargo-binstall-{ target }"
@@ -34,10 +30,8 @@ strum_macros = "0.20.1"
 strum = "0.20.0"
 dirs = "3.0.1"
 serde_derive = "1.0.118"
+crates-index = "0.16.2"
+semver = "0.11.0"
 
 [dev-dependencies]
 env_logger = "0.8.2"
-#github = "0.1.2"
-
-[patch.crates-io]
-#reqwest = { git = "https://github.com/seanmonstar/reqwest.git" }
diff --git a/src/drivers.rs b/src/drivers.rs
index cffe9ad0..9270de59 100644
--- a/src/drivers.rs
+++ b/src/drivers.rs
@@ -2,45 +2,90 @@
 use std::time::Duration;
 use std::path::{Path, PathBuf};
 
-use log::{debug, error};
+use log::{debug};
+use anyhow::{Context, anyhow};
+use semver::{Version, VersionReq};
 
 use crates_io_api::AsyncClient;
 
 use crate::PkgFmt;
 use crate::helpers::*;
 
+fn find_version<'a, V: Iterator<Item=&'a str>>(requirement: &str, version_iter: V) -> Result<String, anyhow::Error> {
+    // Parse version requirement
+    let version_req = VersionReq::parse(requirement)?;
+
+    // Filter for matching versions
+    let mut filtered: Vec<_> = version_iter.filter(|v| {
+        // Remove leading `v` for git tags
+        let ver_str = match v.strip_prefix("s") {
+            Some(v) => v,
+            None => v,
+        };
+
+        // Parse out version
+        let ver = match Version::parse(ver_str) {
+            Ok(sv) => sv,
+            Err(_) => return false,
+        };
+
+        debug!("Version: {:?}", ver);
+
+        // Filter by version match
+        version_req.matches(&ver)
+    }).collect();
+
+    // Sort by highest matching version
+    filtered.sort_by(|a, b| {
+        let a = Version::parse(a).unwrap();
+        let b = Version::parse(b).unwrap();
+
+        b.partial_cmp(&a).unwrap()
+    });
+
+    debug!("Filtered: {:?}", filtered);
+
+    // Return highest version
+    match filtered.get(0) {
+        Some(v) => Ok(v.to_string()),
+        None => Err(anyhow!("No matching version for requirement: '{}'", version_req))
+    }
+}
+
 /// Fetch a crate by name and version from crates.io
-pub async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: &Path) -> Result<PathBuf, anyhow::Error> {
-    // Build crates.io api client and fetch info
-    let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?;
+pub async fn fetch_crate_cratesio(name: &str, version_req: &str, temp_dir: &Path) -> Result<PathBuf, anyhow::Error> {
 
-    debug!("Fetching information for crate: '{}'", name);
+    // Fetch / update index
+    debug!("Updating crates.io index");
+    let index = crates_index::Index::new_cargo_default();
+    index.retrieve_or_update()?;
 
-    // Fetch overall crate info
-    let info = match api_client.get_crate(name.as_ref()).await {
-        Ok(i) => i,
-        Err(e) => {
-            error!("Error fetching information for crate {}: {}", name, e);
-            return Err(e.into())
+    // Lookup crate in index
+    debug!("Looking up crate information");
+    let base_info = match index.crate_(name) {
+        Some(i) => i,
+        None => {
+            return Err(anyhow::anyhow!("Error fetching information for crate {}", name));
         }
     };
 
-    // Use specified or latest version
-    let version_num = match version {
-        Some(v) => v.to_string(),
-        None => info.crate_data.max_version,
-    };
+    // Locate matching version
+    let version_iter = base_info.versions().iter().map(|v| v.version() );
+    let version_name = find_version(version_req, version_iter)?;
+    
+    // Build crates.io api client
+    let api_client = AsyncClient::new("cargo-binstall (https://github.com/ryankurte/cargo-binstall)", Duration::from_millis(100))?;
 
-    // Fetch crates.io information for the specified version
-    // Note it is not viable to use a semver match here as crates.io
-    // appears to elide alpha and yanked versions in the generic response...
-    let versions = info.versions.clone();
-    let version = match versions.iter().find(|v| v.num == version_num) {
+    // Fetch online crate information
+    let crate_info = api_client.get_crate(name.as_ref()).await
+        .context("Error fetching crate information")?;
+
+    // Fetch information for the filtered version
+    let version = match crate_info.versions.iter().find(|v| v.num == version_name) {
         Some(v) => v,
         None => {
-            error!("No crates.io information found for crate: '{}' version: '{}'", 
-                    name, version_num);
-            return Err(anyhow::anyhow!("No crate information found"));
+            return Err(anyhow::anyhow!("No information found for crate: '{}' version: '{}'", 
+                    name, version_name));
         }
     };
 
@@ -58,7 +103,7 @@ pub async fn fetch_crate_cratesio(name: &str, version: Option<&str>, temp_dir: &
     // Decompress downloaded tgz
     debug!("Decompressing crate archive");
     extract(&tgz_path, PkgFmt::Tgz, &temp_dir)?;
-    let crate_path = temp_dir.join(format!("{}-{}", name, version_num));
+    let crate_path = temp_dir.join(format!("{}-{}", name, version_name));
 
     // Return crate directory
     Ok(crate_path)
diff --git a/src/main.rs b/src/main.rs
index d5ce70f5..a3dab985 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,8 +18,8 @@ struct Options {
     name: String,
 
     /// Filter for package version to install
-    #[structopt(long)]
-    version: Option<String>,
+    #[structopt(long, default_value = "*")]
+    version: String,
 
     /// Override binary target, ignoring compiled version
     #[structopt(long, default_value = TARGET)]
@@ -30,14 +30,6 @@ struct Options {
     #[structopt(long)]
     install_path: Option<String>,
 
-    /// Do not cleanup temporary files on success
-    #[structopt(long)]
-    no_cleanup: bool,
-
-    /// Disable interactive mode / confirmation
-    #[structopt(long)]
-    no_confirm: bool,
-
     /// Disable symlinking / versioned updates
     #[structopt(long)]
     no_symlinks: bool,
@@ -46,6 +38,14 @@ struct Options {
     #[structopt(long)]
     dry_run: bool,
 
+    /// Disable interactive mode / confirmation
+    #[structopt(long)]
+    no_confirm: bool,
+
+    /// Do not cleanup temporary files on success
+    #[structopt(long)]
+    no_cleanup: bool,
+
     /// Override manifest source.
     /// This skips searching crates.io for a manifest and uses
     /// the specified path directly, useful for debugging and 
@@ -59,7 +59,6 @@ struct Options {
 }
 
 
-
 #[tokio::main]
 async fn main() -> Result<(), anyhow::Error> {
 
@@ -91,7 +90,7 @@ async fn main() -> Result<(), anyhow::Error> {
     // TODO: support git-based fetches (whole repo name rather than just crate name)
     let manifest_path = match opts.manifest_path.clone() {
         Some(p) => p,
-        None => fetch_crate_cratesio(&opts.name, opts.version.as_deref(), temp_dir.path()).await?,
+        None => fetch_crate_cratesio(&opts.name, &opts.version, temp_dir.path()).await?,
     };
     
     debug!("Reading manifest: {}", manifest_path.display());