1use std::path::Path;
6
7use crate::config::Repository;
8use crate::credentials::{
9 CachedResponse, ResolvedCredentials, ScopedCredentials, SecureHttpClient,
10};
11use crate::error::{RepoError, Result};
12use crate::index::{PackEntry, RepositoryIndex};
13
14pub struct HttpRepository {
16 repo: Repository,
18 client: SecureHttpClient,
20 cached_index: Option<RepositoryIndex>,
22}
23
24impl HttpRepository {
25 pub fn new(repo: Repository, credentials: Option<ResolvedCredentials>) -> Result<Self> {
27 let mut scoped = ScopedCredentials::default();
28 if let Some(creds) = credentials {
29 scoped.add(&repo.url, creds);
30 }
31
32 let client = SecureHttpClient::new(scoped)?;
33
34 Ok(Self {
35 repo,
36 client,
37 cached_index: None,
38 })
39 }
40
41 pub fn public(repo: Repository) -> Result<Self> {
43 Self::new(repo, None)
44 }
45
46 pub fn name(&self) -> &str {
48 &self.repo.name
49 }
50
51 pub fn url(&self) -> &str {
53 &self.repo.url
54 }
55
56 pub async fn fetch_index(&mut self) -> Result<&RepositoryIndex> {
58 let index_url = self.repo.index_url();
59
60 let response = self
62 .client
63 .get_cached(&index_url, self.repo.etag.as_deref())
64 .await?;
65
66 match response {
67 CachedResponse::NotModified => {
68 if self.cached_index.is_none() {
70 return Err(RepoError::CacheError {
71 message: "Received 304 but no cached index".to_string(),
72 });
73 }
74 }
75 CachedResponse::Fresh { data, etag } => {
76 let index = RepositoryIndex::from_bytes(&data)?;
78 self.cached_index = Some(index);
79
80 if etag.is_some() {
82 }
85 }
86 }
87
88 self.cached_index
89 .as_ref()
90 .ok_or(RepoError::IndexNotFound { url: index_url })
91 }
92
93 pub fn index(&self) -> Option<&RepositoryIndex> {
95 self.cached_index.as_ref()
96 }
97
98 pub async fn search(&mut self, query: &str) -> Result<Vec<&PackEntry>> {
100 let index = self.fetch_index().await?;
101 Ok(index.search(query))
102 }
103
104 pub async fn get_latest(&mut self, name: &str) -> Result<PackEntry> {
106 let repo_name = self.repo.name.clone();
107 let index = self.fetch_index().await?;
108 index
109 .get_latest(name)
110 .cloned()
111 .ok_or_else(|| RepoError::PackNotFound {
112 name: name.to_string(),
113 repo: repo_name,
114 })
115 }
116
117 pub async fn get_version(&mut self, name: &str, version: &str) -> Result<PackEntry> {
119 let repo_name = self.repo.name.clone();
120 let index = self.fetch_index().await?;
121 index
122 .get_version(name, version)
123 .cloned()
124 .ok_or_else(|| RepoError::VersionNotFound {
125 name: name.to_string(),
126 version: version.to_string(),
127 repo: repo_name,
128 })
129 }
130
131 pub async fn find_best_match(&mut self, name: &str, constraint: &str) -> Result<PackEntry> {
133 let index = self.fetch_index().await?;
134 index.find_best_match(name, constraint).cloned()
135 }
136
137 pub async fn download(&self, entry: &PackEntry) -> Result<Vec<u8>> {
139 let url = entry
140 .download_url()
141 .ok_or_else(|| RepoError::PackNotFound {
142 name: entry.name.clone(),
143 repo: self.repo.name.clone(),
144 })?;
145
146 let full_url = if url.starts_with("http://") || url.starts_with("https://") {
148 url.to_string()
149 } else {
150 format!("{}/{}", self.repo.url.trim_end_matches('/'), url)
151 };
152
153 let data = self.client.get_bytes(&full_url).await?;
154
155 if let Some(expected_digest) = &entry.digest {
157 let actual_digest = compute_digest(&data);
158 if !digest_matches(expected_digest, &actual_digest) {
159 return Err(RepoError::IntegrityCheckFailed {
160 name: entry.name.clone(),
161 expected: expected_digest.clone(),
162 actual: actual_digest,
163 });
164 }
165 }
166
167 Ok(data)
168 }
169
170 pub async fn download_to(&self, entry: &PackEntry, dest: &Path) -> Result<()> {
172 let data = self.download(entry).await?;
173
174 extract_pack_archive(&data, dest)?;
176
177 Ok(())
178 }
179
180 pub async fn list(&mut self) -> Result<Vec<&PackEntry>> {
182 let index = self.fetch_index().await?;
183 Ok(index
184 .entries
185 .values()
186 .filter_map(|versions| {
187 versions.iter().max_by(|a, b| {
188 let va = semver::Version::parse(&a.version).ok();
189 let vb = semver::Version::parse(&b.version).ok();
190 va.cmp(&vb)
191 })
192 })
193 .collect())
194 }
195}
196
197fn compute_digest(data: &[u8]) -> String {
199 use sha2::{Digest, Sha256};
200 let mut hasher = Sha256::new();
201 hasher.update(data);
202 let result = hasher.finalize();
203 format!("sha256:{}", hex::encode(result))
204}
205
206fn digest_matches(expected: &str, actual: &str) -> bool {
208 let norm_expected = expected
210 .trim()
211 .to_lowercase()
212 .replace("sha256:", "")
213 .replace("sha256-", "");
214 let norm_actual = actual
215 .trim()
216 .to_lowercase()
217 .replace("sha256:", "")
218 .replace("sha256-", "");
219
220 norm_expected == norm_actual
221}
222
223fn extract_pack_archive(data: &[u8], dest: &Path) -> Result<()> {
225 use flate2::read::GzDecoder;
226 use tar::Archive;
227
228 let gz = GzDecoder::new(std::io::Cursor::new(data));
229 let mut archive = Archive::new(gz);
230
231 std::fs::create_dir_all(dest)?;
232 archive.unpack(dest)?;
233
234 Ok(())
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_compute_digest() {
243 let data = b"hello world";
244 let digest = compute_digest(data);
245 assert!(digest.starts_with("sha256:"));
246 assert_eq!(digest.len(), 7 + 64); }
248
249 #[test]
250 fn test_digest_matches() {
251 let d1 = "sha256:abc123";
252 let d2 = "sha256:ABC123";
253 let d3 = "abc123";
254 let d4 = "sha256-abc123";
255
256 assert!(digest_matches(d1, d2));
257 assert!(digest_matches(d1, d3));
258 assert!(digest_matches(d1, d4));
259 assert!(!digest_matches(d1, "sha256:xyz789"));
260 }
261}