cargo_unmaintained/github/real/
mod.rs

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