cargo_unmaintained/github/real/
mod.rs

1use crate::{RepoStatus, Url, curl};
2use anyhow::{Result, bail};
3use regex::Regex;
4use std::{cell::RefCell, collections::HashMap, io::Read, rc::Rc, sync::LazyLock};
5
6mod map_ext;
7use map_ext::MapExt;
8
9pub mod util;
10
11#[allow(clippy::unwrap_used)]
12static RE: LazyLock<Regex> =
13    LazyLock::new(|| Regex::new(r"^https://github\.com/(([^/]*)/([^/]*))").unwrap());
14
15thread_local! {
16    static REPOSITORY_CACHE: RefCell<HashMap<String, Option<Rc<serde_json::Value>>>> = RefCell::new(HashMap::new());
17}
18
19pub struct Impl;
20
21impl super::Github for Impl {
22    fn load_token(f: impl FnOnce(&str) -> Result<()>) -> Result<bool> {
23        util::load_token(f)
24    }
25
26    fn save_token() -> Result<()> {
27        util::save_token()
28    }
29
30    fn archival_status(url: Url) -> Result<RepoStatus<()>> {
31        let (url, owner_slash_repo, owner, repo) = match_github_url(url)?;
32
33        let Some(repository) = repository(owner_slash_repo, owner, repo)? else {
34            return Ok(RepoStatus::Nonexistent(url));
35        };
36
37        if repository
38            .as_object()
39            .and_then(|map| map.get_bool("archived"))
40            .unwrap_or_default()
41        {
42            Ok(RepoStatus::Archived(url))
43        } else {
44            Ok(RepoStatus::Success(url, ()))
45        }
46    }
47}
48
49#[cfg_attr(dylint_lib = "general", allow(non_local_effect_before_error_return))]
50// smoelius: `owner_slash_repo` is a hack to avoid calling `to_owned` on `owner` and `repo` just to
51// perform a cache lookup.
52fn repository(
53    owner_slash_repo: &str,
54    owner: &str,
55    repo: &str,
56) -> Result<Option<Rc<serde_json::Value>>> {
57    REPOSITORY_CACHE.with_borrow_mut(|repository_cache| {
58        if let Some(repo) = repository_cache.get(owner_slash_repo) {
59            return Ok(repo.clone());
60        }
61
62        match repository_uncached(owner, repo) {
63            Ok(repository) => Ok(repository_cache
64                .entry(owner_slash_repo.to_owned())
65                .or_insert(Some(Rc::new(repository)))
66                .clone()),
67            Err(error) => {
68                repository_cache.insert(owner_slash_repo.to_owned(), None);
69                Err(error)
70            }
71        }
72    })
73}
74
75fn repository_uncached(owner: &str, repo: &str) -> Result<serde_json::Value> {
76    call_api(owner, repo, None, &[])
77}
78
79fn match_github_url(url: Url<'_>) -> Result<(Url<'_>, &str, &str, &str)> {
80    let (url_string, owner_slash_repo, owner, repo) = {
81        #[allow(clippy::unwrap_used)]
82        if let Some(captures) = RE.captures(url.as_str()) {
83            assert_eq!(4, captures.len());
84            (
85                captures.get(0).unwrap().as_str(),
86                captures.get(1).unwrap().as_str(),
87                captures.get(2).unwrap().as_str(),
88                captures.get(3).unwrap().as_str(),
89            )
90        } else {
91            bail!("failed to match GitHub url: {url}");
92        }
93    };
94
95    let repo = repo.strip_suffix(".git").unwrap_or(repo);
96
97    Ok((url_string.into(), owner_slash_repo, owner, repo))
98}
99
100fn call_api(
101    owner: &str,
102    repo: &str,
103    endpoint: Option<&str>,
104    mut data: &[u8],
105) -> Result<serde_json::Value> {
106    let url_string = format!(
107        "https://api.github.com/repos/{owner}/{repo}{}",
108        endpoint
109            .map(|endpoint| String::from("/") + endpoint)
110            .unwrap_or_default(),
111    );
112
113    let mut list = ::curl::easy::List::new();
114    list.append("User-Agent: cargo-unmaintained")?;
115    if let Some(token) = util::PERSONAL_TOKEN.get() {
116        list.append(&format!("Authorization: Bearer {token}"))?;
117    }
118
119    let mut handle = curl::handle(url_string.as_str().into())?;
120    handle.http_headers(list)?;
121    let mut response = Vec::new();
122    {
123        let mut transfer = handle.transfer();
124        transfer.read_function(|buf| {
125            #[allow(clippy::unwrap_used)]
126            let len = data.read(buf).unwrap();
127            Ok(len)
128        })?;
129        transfer.write_function(|other| {
130            response.extend_from_slice(other);
131            Ok(other.len())
132        })?;
133        transfer.perform()?;
134    }
135
136    let response_code = handle.response_code()?;
137
138    // smoelius: Should the next statement handle 404s, like `curl::existence` does?
139    if response_code != 200 {
140        bail!("unexpected response code: {response_code}");
141    }
142
143    let value = serde_json::from_slice::<serde_json::Value>(&response)?;
144
145    Ok(value)
146}