haste_fhir_package/
registry.rs1use crate::{PackageError, valid_name, valid_version};
10use reqwest::{Url, redirect};
11use serde::Deserialize;
12use std::{collections::BTreeMap, sync::LazyLock, time::Duration};
13
14const MAX_REDIRECTS: usize = 5;
16
17static HTTP: LazyLock<reqwest::Client> = LazyLock::new(|| {
18 reqwest::Client::builder()
19 .connect_timeout(Duration::from_secs(10))
20 .timeout(Duration::from_mins(2))
21 .https_only(true)
22 .redirect(redirect::Policy::custom(within_registry))
23 .user_agent(concat!("haste-health/", env!("CARGO_PKG_VERSION")))
24 .build()
25 .expect("failed to build the package registry HTTP client")
26});
27
28fn within_registry(attempt: redirect::Attempt) -> redirect::Action {
30 let start = attempt.previous().first().map(Url::origin);
31 if start.is_none_or(|start| start != attempt.url().origin()) {
32 attempt.error("the registry redirected to another host")
33 } else if attempt.previous().len() > MAX_REDIRECTS {
34 attempt.error("too many redirects")
35 } else {
36 attempt.follow()
37 }
38}
39
40fn registry_base(registry: &str) -> Result<Url, PackageError> {
43 let registry = registry.trim();
44 if registry.is_empty() {
45 return Err(PackageError::RegistryDisabled);
46 }
47 let base = Url::parse(registry).map_err(|_| PackageError::InvalidRegistry)?;
48 if base.scheme() != "https"
49 || base.host().is_none()
50 || base.query().is_some()
51 || base.fragment().is_some()
52 {
53 return Err(PackageError::InvalidRegistry);
54 }
55 Ok(base)
56}
57
58#[derive(Deserialize, Default)]
60#[serde(default)]
61struct PackageVersions {
62 #[serde(rename = "dist-tags")]
63 dist_tags: BTreeMap<String, String>,
64 versions: BTreeMap<String, serde_json::Value>,
65}
66
67fn url(registry: &Url, segments: &[&str]) -> Url {
68 let mut url = registry.clone();
69 if let Ok(mut path) = url.path_segments_mut() {
70 path.pop_if_empty().extend(segments);
71 }
72 url
73}
74
75fn registry_error(error: reqwest::Error) -> PackageError {
78 let error = error.without_url();
79 let mut message = error.to_string();
80 let mut source = std::error::Error::source(&error);
81 while let Some(cause) = source {
82 message.push_str(": ");
83 message.push_str(&cause.to_string());
84 source = cause.source();
85 }
86 PackageError::Registry(message)
87}
88
89fn is_exact(requested: &str) -> bool {
92 requested != "latest"
93 && !requested
94 .split(['.', '-', '+'])
95 .any(|part| matches!(part, "x" | "X" | "*"))
96}
97
98fn newest_match<'a>(versions: impl Iterator<Item = &'a String>, requested: &str) -> Option<String> {
101 let prefix = requested
102 .split('.')
103 .take_while(|part| !matches!(*part, "x" | "X" | "*"))
104 .fold(String::new(), |mut prefix, part| {
105 prefix.push_str(part);
106 prefix.push('.');
107 prefix
108 });
109 versions
110 .filter(|version| version.starts_with(&prefix))
111 .filter_map(|version| {
112 semver::Version::parse(version)
113 .ok()
114 .filter(|parsed| parsed.pre.is_empty())
115 .map(|parsed| (parsed, version))
116 })
117 .max_by(|(a, _), (b, _)| a.cmp(b))
118 .map(|(_, version)| version.clone())
119}
120
121pub async fn resolve_version(
131 registry: &str,
132 name: &str,
133 requested: Option<&str>,
134) -> Result<String, PackageError> {
135 let registry = registry_base(registry)?;
136 if !valid_name(name) {
137 return Err(PackageError::InvalidName(name.to_string()));
138 }
139 if let Some(requested) = requested {
140 if !valid_version(requested) {
141 return Err(PackageError::InvalidVersion(requested.to_string()));
142 }
143 if is_exact(requested) {
144 return Ok(requested.to_string());
145 }
146 }
147
148 let response = HTTP
149 .get(url(®istry, &[name]))
150 .header("Accept", "application/json")
151 .send()
152 .await
153 .map_err(registry_error)?;
154 if response.status() == reqwest::StatusCode::NOT_FOUND {
155 return Err(PackageError::NotFound(name.to_string()));
156 }
157 let body = response
158 .error_for_status()
159 .map_err(registry_error)?
160 .bytes()
161 .await
162 .map_err(registry_error)?;
163 let versions = serde_json::from_slice::<PackageVersions>(&body)
164 .map_err(|e| PackageError::Registry(format!("unexpected response for '{name}': {e}")))?;
165
166 let requested = requested.unwrap_or("latest");
167 let resolved = if requested == "latest" {
168 versions.dist_tags.get("latest").cloned()
169 } else {
170 newest_match(versions.versions.keys(), requested)
171 };
172 resolved.ok_or_else(|| PackageError::NoMatchingVersion(name.to_string(), requested.to_string()))
173}
174
175pub async fn download(
184 registry: &str,
185 name: &str,
186 version: &str,
187 max_bytes: u64,
188) -> Result<Vec<u8>, PackageError> {
189 let registry = registry_base(registry)?;
190 if !valid_name(name) {
191 return Err(PackageError::InvalidName(name.to_string()));
192 }
193 if !valid_version(version) {
194 return Err(PackageError::InvalidVersion(version.to_string()));
195 }
196
197 let mut response = HTTP
198 .get(url(®istry, &[name, version]))
199 .send()
200 .await
201 .map_err(registry_error)?;
202 if response.status() == reqwest::StatusCode::NOT_FOUND {
203 return Err(PackageError::NotFound(format!("{name}@{version}")));
204 }
205 response = response.error_for_status().map_err(registry_error)?;
206 if response
207 .content_length()
208 .is_some_and(|length| length > max_bytes)
209 {
210 return Err(PackageError::TooLarge(max_bytes));
211 }
212
213 let mut bytes = Vec::new();
214 while let Some(chunk) = response.chunk().await.map_err(registry_error)? {
215 if (bytes.len() + chunk.len()) as u64 > max_bytes {
216 return Err(PackageError::TooLarge(max_bytes));
217 }
218 bytes.extend_from_slice(&chunk);
219 }
220 Ok(bytes)
221}