From 130fb77d22f86f70e5084c2c222ff3d77965918f Mon Sep 17 00:00:00 2001
From: Jake Shadle <jake.shadle@embark-studios.com>
Date: Fri, 3 May 2024 15:30:12 +0200
Subject: [PATCH] Add IndexUrl::for_registry_name (#61)

Resolves: #60
---
 src/error.rs          |   7 ++
 src/index/location.rs | 144 ++++++++++++++++++++++++++++++++++++++++--
 src/utils/flock.rs    |   2 +-
 3 files changed, 148 insertions(+), 5 deletions(-)

diff --git a/src/error.rs b/src/error.rs
index 43e19a9..72acad1 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -15,9 +15,16 @@ pub enum Error {
     /// was not valid utf-8
     #[error("unable to use non-utf8 path {:?}", .0)]
     NonUtf8Path(std::path::PathBuf),
+    /// An environment variable was located, but had a non-utf8 value
+    #[error("environment variable {} has a non-utf8 value", .0)]
+    NonUtf8EnvVar(std::borrow::Cow<'static, str>),
     /// A user-provided string was not a valid crate name
     #[error(transparent)]
     InvalidKrateName(#[from] InvalidKrateName),
+    /// The user specified a registry name that did not exist in any searched
+    /// .cargo/config.toml
+    #[error("registry '{}' was not located in any .cargo/config.toml", .0)]
+    UnknownRegistry(String),
     /// An I/O error
     #[error(transparent)]
     Io(#[from] std::io::Error),
diff --git a/src/index/location.rs b/src/index/location.rs
index 2d004bb..ed99763 100644
--- a/src/index/location.rs
+++ b/src/index/location.rs
@@ -57,7 +57,9 @@ impl<'iu> IndexUrl<'iu> {
     ) -> Result<Self, Error> {
         // If the crates.io registry has been replaced it doesn't matter what
         // the protocol for it has been changed to
-        if let Some(replacement) = get_crates_io_replacement(config_root.clone(), cargo_home)? {
+        if let Some(replacement) =
+            get_source_replacement(config_root.clone(), cargo_home, "crates-io")?
+        {
             return Ok(replacement);
         }
 
@@ -99,6 +101,64 @@ impl<'iu> IndexUrl<'iu> {
             Self::CratesIoGit
         })
     }
+
+    /// Creates an [`IndexUrl`] for the specified registry name
+    ///
+    /// 1. Checks if [`CARGO_REGISTRIES_<name>_INDEX`](https://doc.rust-lang.org/cargo/reference/config.html#registriesnameindex) is set
+    /// 2. Checks if the source for the registry has been [replaced](https://doc.rust-lang.org/cargo/reference/source-replacement.html)
+    /// 3. Uses the value of [`registries.<name>.index`](https://doc.rust-lang.org/cargo/reference/config.html#registriesnameindex) otherwise
+    pub fn for_registry_name(
+        config_root: Option<PathBuf>,
+        cargo_home: Option<&Path>,
+        registry_name: &str,
+    ) -> Result<Self, Error> {
+        // Check if the index was explicitly specified
+        let mut env = String::with_capacity(17 + registry_name.len() + 6);
+        env.push_str("CARGO_REGISTRIES_");
+
+        if registry_name.is_ascii() {
+            for c in registry_name.chars() {
+                if c == '-' {
+                    env.push('_');
+                } else {
+                    env.push(c.to_ascii_uppercase());
+                }
+            }
+        } else {
+            let mut upper = registry_name.to_uppercase();
+            if upper.contains('-') {
+                upper = upper.replace('-', "_");
+            }
+
+            env.push_str(&upper);
+        }
+
+        env.push_str("_INDEX");
+
+        match std::env::var(&env) {
+            Ok(index) => return Ok(Self::NonCratesIo(index.into())),
+            Err(err) => {
+                if let std::env::VarError::NotUnicode(_nu) = err {
+                    return Err(Error::NonUtf8EnvVar(env.into()));
+                }
+            }
+        }
+
+        if let Some(replacement) =
+            get_source_replacement(config_root.clone(), cargo_home, registry_name)?
+        {
+            return Ok(replacement);
+        }
+
+        read_cargo_config(config_root, cargo_home, |config| {
+            let path = format!("/registries/{registry_name}/index");
+            config
+                .pointer(&path)?
+                .as_str()
+                .map(|si| Self::NonCratesIo(si.to_owned().into()))
+        })?
+        .ok_or_else(|| Error::UnknownRegistry(registry_name.into()))
+    }
 }
 
 impl<'iu> From<&'iu str> for IndexUrl<'iu> {
@@ -242,16 +302,18 @@ pub(crate) fn read_cargo_config<T>(
     Ok(None)
 }
 
-/// Gets the url of a replacement registry for crates.io if one has been configured
+/// Gets the url of a replacement registry for the specified registry if one has been configured
 ///
 /// See <https://doc.rust-lang.org/cargo/reference/source-replacement.html>
 #[inline]
-pub(crate) fn get_crates_io_replacement<'iu>(
+pub(crate) fn get_source_replacement<'iu>(
     root: Option<PathBuf>,
     cargo_home: Option<&Path>,
+    registry_name: &str,
 ) -> Result<Option<IndexUrl<'iu>>, Error> {
     read_cargo_config(root, cargo_home, |config| {
-        let repw = config.pointer("/source/crates-io/replace-with")?.as_str()?;
+        let path = format!("/source/{registry_name}/replace-with");
+        let repw = config.pointer(&path)?.as_str()?;
         let sources = config.pointer("/source")?.as_table()?;
         let replace_src = sources.get(&repw.into())?.as_table()?;
 
@@ -324,4 +386,78 @@ protocol = "git"
             assert_eq!(iurl.as_str(), *url);
         }
     }
+
+    #[test]
+    fn custom() {
+        assert!(std::env::var_os("CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX").is_none());
+
+        let td = tempfile::tempdir().unwrap();
+        let root = crate::PathBuf::from_path_buf(td.path().to_owned()).unwrap();
+        let cfg_toml = td.path().join(".cargo/config.toml");
+
+        std::fs::create_dir_all(cfg_toml.parent().unwrap()).unwrap();
+
+        const SPARSE: &str = r#"[registries.tame-index-test]
+index = "sparse+https://some-url.com"
+"#;
+
+        const GIT: &str = r#"[registries.tame-index-test]
+        index = "https://some-url.com"
+        "#;
+
+        {
+            std::fs::write(&cfg_toml, SPARSE).unwrap();
+
+            let iurl =
+                super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
+                    .unwrap();
+            assert_eq!(iurl.as_str(), "sparse+https://some-url.com");
+            assert!(iurl.is_sparse());
+
+            std::env::set_var(
+                "CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX",
+                "sparse+https://some-other-url.com",
+            );
+
+            let iurl =
+                super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
+                    .unwrap();
+            assert_eq!(iurl.as_str(), "sparse+https://some-other-url.com");
+            assert!(iurl.is_sparse());
+
+            std::env::remove_var("CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX");
+        }
+
+        {
+            std::fs::write(&cfg_toml, GIT).unwrap();
+
+            let iurl =
+                super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
+                    .unwrap();
+            assert_eq!(iurl.as_str(), "https://some-url.com");
+            assert!(!iurl.is_sparse());
+
+            std::env::set_var(
+                "CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX",
+                "https://some-other-url.com",
+            );
+
+            let iurl =
+                super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
+                    .unwrap();
+            assert_eq!(iurl.as_str(), "https://some-other-url.com");
+            assert!(!iurl.is_sparse());
+
+            std::env::remove_var("CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX");
+        }
+
+        #[allow(unused_variables)]
+        {
+            let err = crate::Error::UnknownRegistry("non-existant".to_owned());
+            assert!(matches!(
+                super::IndexUrl::for_registry_name(Some(root.clone()), None, "non-existant"),
+                Err(err),
+            ));
+        }
+    }
 }
diff --git a/src/utils/flock.rs b/src/utils/flock.rs
index 7cebd65..1988cb7 100644
--- a/src/utils/flock.rs
+++ b/src/utils/flock.rs
@@ -30,7 +30,7 @@ pub enum LockError {
     #[error("failed to create parent directories for lock path")]
     CreateDir(std::io::Error),
     /// Locking is not supported if the lock file is on an NFS, though note this
-    /// is a bit more nuanced as NFSv4 _does_ support file locking, but is out
+    /// is a bit more nuanced as `NFSv4` _does_ support file locking, but is out
     /// of scope, at least for now
     #[error("NFS do not support locking")]
     Nfs,