cargo_unmaintained/github/real/
mod.rs1use 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))]
53fn 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 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}