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