Repository: trailofbits/cargo-unmaintained (language: rust)

Issue:
Dont treat Mercurial repos as an error 
In a similar vein as #279 , I see the following for https://crates.io/crates/oorandom

```
warning: failed to clone `https://hg.sr.ht/~icefox/oorandom`: [
    "fatal: repository 'https://hg.sr.ht/~icefox/oorandom/' not found\n",
]
...
oorandom (https://hg.sr.ht/~icefox/oorandom is uncloneable)
```

It is maintained, recently fixing the metadata - c.f. https://todo.sr.ht/~icefox/oorandom/8

While I can appreciate that there may not be much benefit in adding support for Mercurial repos, and a processing warning is useful, the crate should be "unmaintained" simply because it is using Mercurial.

A list of hg hosting platforms can be found at https://wiki.mercurial-scm.org/MercurialHosting . Some like https://hg.sr.ht/ and https://hg.mozilla.org/ and http://hg.code.sf.net/ include "hg." prefix which could be used to detect it.

However others dont have an easy way to detect  e.g. https://crates.io/crates/bytes-cast


Discussion / hints:
Note to self: the report comes from here: https://github.com/trailofbits/cargo-unmaintained/blob/1994fac346cdc06705fe8b6981136d68734beb62/src/main.rs#L458

That is, to be considered maintained, a package must be a member of its named repository. And to determine whether that is the case, `cargo-unmaintained` must clone the repository.

Fix patch:
diff --git a/README.md b/README.md
index 4f7999ba..1c815d20 100644
--- a/README.md
+++ b/README.md
@@ -111,8 +111,6 @@ cargo test --features=ei
 
 ## Known problems
 
-- Mercurial repositories are considered "uncloneable" and therefore unmaintained. ([#369])
-
 - If a package is renamed from X to Y, it is immediately considered unmaintained because the package's repository no longer contains a package named X. ([#441])
 
 - If a project relies on an old version of a package, `cargo-unmaintained` may fail to flag the package as unmaintained (i.e., may produce a false negative). The following is a sketch of how this can occur.
@@ -142,7 +140,6 @@ We reserve the right to change the following and to consider such changes non-br
 
 `cargo-unmaintained` is licensed and distributed under the AGPLv3 license. [Contact us](mailto:opensource@trailofbits.com) if you're looking for an exception to the terms.
 
-[#369]: https://github.com/trailofbits/cargo-unmaintained/issues/369
 [#441]: https://github.com/trailofbits/cargo-unmaintained/issues/441
 [Cargo 0.74.0]: https://github.com/rust-lang/cargo/tree/d252bce6553c8cc521840c9dd6b9f6cd4aedd8b0
 [Notes]: #notes
diff --git a/fixtures/mercurial/Cargo.lock b/fixtures/mercurial/Cargo.lock
new file mode 100644
index 00000000..ff2984d8
--- /dev/null
+++ b/fixtures/mercurial/Cargo.lock
@@ -0,0 +1,16 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "mercurial"
+version = "0.1.0"
+dependencies = [
+ "oorandom",
+]
+
+[[package]]
+name = "oorandom"
+version = "11.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
diff --git a/fixtures/mercurial/Cargo.toml b/fixtures/mercurial/Cargo.toml
new file mode 100644
index 00000000..1463a7e9
--- /dev/null
+++ b/fixtures/mercurial/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "mercurial"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+oorandom = "11.1"
diff --git a/fixtures/mercurial/src/lib.rs b/fixtures/mercurial/src/lib.rs
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/fixtures/mercurial/src/lib.rs
@@ -0,0 +1,1 @@
+
diff --git a/src/curl.rs b/src/curl.rs
index 54feeb14..8cf9783f 100644
--- a/src/curl.rs
+++ b/src/curl.rs
@@ -1,6 +1,6 @@
 use super::{RepoStatus, Url};
 use anyhow::{anyhow, Result};
-use curl::easy::Easy;
+use curl::easy::{Easy, List};
 use std::time::Duration;
 
 const TIMEOUT: u64 = 60; // seconds
@@ -17,6 +17,41 @@ pub(crate) fn existence(url: Url) -> Result<RepoStatus<()>> {
     }
 }
 
+// smoelius: As of this writing, the Mercurial repository contains the following Python code:
+//
+//     try:
+//         proto = pycompat.bytesurl(resp.getheader('content-type', ''))
+//     except AttributeError:
+//         proto = pycompat.bytesurl(resp.headers.get('content-type', ''))
+//
+//     ...
+//
+//     if not proto.startswith(b'application/mercurial-'):
+//         ui.debug(b"requested URL: '%s'\n" % urlutil.hidepassword(requrl))
+//         msg = _(
+//             b"'%s' does not appear to be an hg repository:\n"
+//             b"---%%<--- (%s)\n%s\n---%%<---\n"
+//         ) % (safeurl, proto or b'no content-type', resp.read(1024))
+//
+// Thus, checking the content type seems to be as good a method as any for determining whether a url
+// refers to a Mercurial repository.
+//
+// Reference: https://repo.mercurial-scm.org/hg/file/5cc8deb96b48/mercurial/httppeer.py#l342
+pub(crate) fn is_mercurial_repo(url: Url) -> Result<bool> {
+    let url_string = url.to_string() + "?cmd=capabilities";
+
+    let mut list = List::new();
+    list.append("Accept: application/mercurial-0.1")?;
+
+    let mut handle = handle(url_string.as_str().into())?;
+    handle.http_headers(list)?;
+    handle.transfer().perform()?;
+
+    let content_type = handle.content_type()?;
+
+    Ok(content_type.is_some_and(|content_type| content_type.starts_with("application/mercurial-")))
+}
+
 pub(crate) fn handle(url: Url) -> Result<Easy> {
     let mut handle = Easy::new();
     handle.url(url.as_str())?;
diff --git a/src/lib.rs b/src/lib.rs
index af607770..5628d47a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -472,8 +472,10 @@ fn is_unmaintained_package<'a>(
         let can_use_github_api =
             TOKEN_FOUND.load(Ordering::SeqCst) && url_string.starts_with("https://github.com/");
 
+        let url = url_string.as_str().into();
+
         if can_use_github_api {
-            let repo_status = general_status(&pkg.name, url_string.as_str().into())?;
+            let repo_status = general_status(&pkg.name, url)?;
             if repo_status.is_failure() {
                 return Ok(Some(UnmaintainedPkg {
                     pkg,
@@ -486,6 +488,10 @@ fn is_unmaintained_package<'a>(
 
         let repo_status = clone_repository(pkg, Purpose::Membership)?;
         if repo_status.is_failure() {
+            // smoelius: Mercurial repos get a pass.
+            if matches!(repo_status, RepoStatus::Uncloneable(_)) && curl::is_mercurial_repo(url)? {
+                return Ok(None);
+            }
             return Ok(Some(UnmaintainedPkg {
                 pkg,
                 repo_age: repo_status.map_failure(),


Test patch:
diff --git a/tests/cases/cargo.json b/tests/cases/cargo.json
index 10c35c4a..6e81ac80 100644
--- a/tests/cases/cargo.json
+++ b/tests/cases/cargo.json
@@ -41,12 +41,6 @@
     },
     "version": "0.6.0"
   },
-  {
-    "name": "oorandom",
-    "outdated_deps": [],
-    "repo_status": "Uncloneable",
-    "version": "11.1.3"
-  },
   {
     "name": "partial_ref_derive",
     "outdated_deps": [
diff --git a/tests/cases/mercurial.json b/tests/cases/mercurial.json
new file mode 100644
index 00000000..fe51488c
--- /dev/null
+++ b/tests/cases/mercurial.json
@@ -0,0 +1,1 @@
+[]
diff --git a/tests/cases/mercurial.stderr b/tests/cases/mercurial.stderr
new file mode 100644
index 00000000..338d776b
--- /dev/null
+++ b/tests/cases/mercurial.stderr
@@ -0,0 +1,4 @@
+Scanning 1 packages and their dependencies (pass --verbose for more information)
+warning: failed to clone `https://hg.sr.ht/~icefox/oorandom`: [
+    "fatal: repository 'https://hg.sr.ht/~icefox/oorandom/' not found/n",
+]
diff --git a/tests/cases/mercurial.stdout b/tests/cases/mercurial.stdout
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/cases/mercurial.toml b/tests/cases/mercurial.toml
new file mode 100644
index 00000000..e7934eb0
--- /dev/null
+++ b/tests/cases/mercurial.toml
@@ -0,0 +1,1 @@
+path = "fixtures/mercurial"
diff --git a/tests/cases/taplo.json b/tests/cases/taplo.json
index ca21859d..507748b8 100644
--- a/tests/cases/taplo.json
+++ b/tests/cases/taplo.json
@@ -11,12 +11,6 @@
     "repo_status": "Unassociated",
     "version": "0.1.19"
   },
-  {
-    "name": "oorandom",
-    "outdated_deps": [],
-    "repo_status": "Uncloneable",
-    "version": "11.1.3"
-  },
   {
     "name": "proc-macro-error",
     "outdated_deps": [