1mod cache_paths;
2pub mod store;
3
4pub use cache_paths::{
5 DownloadDirectoryFallback, DownloadDirectoryKind, PreparedDownloadDirectories,
6 download_cache_diagnostic, download_cache_diagnostic_for, huggingface_hub_cache_dir,
7 huggingface_xet_cache_dir, mesh_llm_cache_dir, prepare_download_directories,
8};
9
10use std::{
11 ffi::OsStr,
12 path::{Path, PathBuf},
13 time::Duration,
14};
15
16use anyhow::{Context, Result};
17use async_trait::async_trait;
18use hf_hub::{
19 HFClient, HFClientBuilder, RepoType, RepoTypeModel,
20 cache::{CachedRepoInfo, HFCacheInfo},
21 repository::ModelInfo,
22};
23use model_artifact::{ModelArtifactFile, ModelIdentity, ModelRepository, ResolvedModelArtifact};
24use model_ref::{
25 format_canonical_ref, format_model_ref, normalize_gguf_distribution_id,
26 quant_selector_from_gguf_file,
27};
28use serde::{Deserialize, Serialize};
29
30const HF_RETRY_MAX_ATTEMPTS_ENV: &str = "MESH_HF_RETRY_MAX_ATTEMPTS";
31const HF_RETRY_BASE_DELAY_MS_ENV: &str = "MESH_HF_RETRY_BASE_DELAY_MS";
32
33#[derive(Clone)]
34pub struct HfModelRepository {
35 api: HFClient,
36 cache_dir: PathBuf,
37}
38
39impl HfModelRepository {
40 pub fn from_env() -> Result<Self> {
41 Self::builder().build()
42 }
43
44 pub fn builder() -> HfModelRepositoryBuilder {
45 HfModelRepositoryBuilder::default()
46 }
47
48 pub fn cache_dir(&self) -> &Path {
49 &self.cache_dir
50 }
51
52 pub async fn download_file(&self, repo: &str, revision: &str, file: &str) -> Result<PathBuf> {
53 let (owner, name) = repo_parts(repo);
54 self.api
55 .model(owner, name)
56 .download_file()
57 .filename(file.to_string())
58 .revision(revision.to_string())
59 .send()
60 .await
61 .with_context(|| {
62 format!(
63 "download Hugging Face model file {repo}@{revision}/{file}. {}",
64 download_cache_diagnostic_for(&self.cache_dir)
65 )
66 })
67 }
68
69 pub async fn download_artifact_files(
70 &self,
71 artifact: &ResolvedModelArtifact,
72 ) -> Result<Vec<PathBuf>> {
73 let mut paths = Vec::with_capacity(artifact.files.len());
74 for file in &artifact.files {
75 paths.push(
76 self.download_file(&artifact.source_repo, &artifact.source_revision, &file.path)
77 .await?,
78 );
79 }
80 Ok(paths)
81 }
82
83 pub fn identity_for_path(&self, path: &Path) -> Option<HfModelIdentity> {
84 huggingface_identity_for_path_in_cache(path, &self.cache_dir)
85 }
86}
87
88#[derive(Default)]
89pub struct HfModelRepositoryBuilder {
90 cache_dir: Option<PathBuf>,
91 endpoint: Option<String>,
92 token: Option<String>,
93 retry_max_attempts: Option<usize>,
94 retry_base_delay: Option<Duration>,
95}
96
97impl HfModelRepositoryBuilder {
98 pub fn cache_dir(mut self, cache_dir: impl Into<PathBuf>) -> Self {
99 self.cache_dir = Some(cache_dir.into());
100 self
101 }
102
103 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
104 self.endpoint = Some(endpoint.into());
105 self
106 }
107
108 pub fn token(mut self, token: impl Into<String>) -> Self {
109 self.token = Some(token.into());
110 self
111 }
112
113 pub fn retry_max_attempts(mut self, max_attempts: usize) -> Self {
114 self.retry_max_attempts = Some(max_attempts);
115 self
116 }
117
118 pub fn retry_base_delay(mut self, delay: Duration) -> Self {
119 self.retry_base_delay = Some(delay);
120 self
121 }
122
123 pub fn build(self) -> Result<HfModelRepository> {
124 let cache_dir = self.cache_dir.unwrap_or_else(huggingface_hub_cache_dir);
125 let mut builder = HFClientBuilder::new()
126 .cache_dir(cache_dir.clone())
127 .retry_max_attempts(6)
128 .retry_base_delay(Duration::from_millis(500));
129
130 let endpoint = self
131 .endpoint
132 .or_else(|| std::env::var("HF_ENDPOINT").ok())
133 .map(|endpoint| endpoint.trim().to_string())
134 .filter(|endpoint| !endpoint.is_empty());
135 if let Some(endpoint) = endpoint {
136 builder = builder.endpoint(endpoint);
137 }
138
139 let token = self.token.or_else(hf_token_override);
140 if let Some(token) = token {
141 builder = builder.token(token);
142 }
143
144 let retry_max_attempts = self
145 .retry_max_attempts
146 .or_else(|| env_usize(HF_RETRY_MAX_ATTEMPTS_ENV));
147 if let Some(max_attempts) = retry_max_attempts {
148 builder = builder.retry_max_attempts(max_attempts);
149 }
150
151 let retry_base_delay = self
152 .retry_base_delay
153 .or_else(|| env_duration_millis(HF_RETRY_BASE_DELAY_MS_ENV));
154 if let Some(delay) = retry_base_delay {
155 builder = builder.retry_base_delay(delay);
156 }
157
158 let api = builder.build().context("build Hugging Face API client")?;
159 Ok(HfModelRepository { api, cache_dir })
160 }
161}
162
163#[async_trait]
164impl ModelRepository for HfModelRepository {
165 async fn resolve_revision(&self, repo: &str, revision: Option<&str>) -> Result<String> {
166 let revision = revision.unwrap_or("main");
167 self.repo_info(repo, revision)
168 .await?
169 .sha
170 .with_context(|| format!("Hugging Face repo {repo}@{revision} did not return a sha"))
171 }
172
173 async fn list_files(&self, repo: &str, revision: &str) -> Result<Vec<ModelArtifactFile>> {
174 let info = self.repo_info(repo, revision).await?;
175 Ok(info
176 .siblings
177 .unwrap_or_default()
178 .into_iter()
179 .map(|sibling| ModelArtifactFile {
180 path: sibling.rfilename,
181 size_bytes: sibling.size,
182 sha256: None,
183 })
184 .collect())
185 }
186}
187
188impl HfModelRepository {
189 async fn repo_info(&self, repo: &str, revision: &str) -> Result<ModelInfo> {
190 let (owner, name) = repo_parts(repo);
191 self.api
192 .model(owner, name)
193 .info()
194 .revision(revision.to_string())
195 .send()
196 .await
197 .with_context(|| format!("fetch Hugging Face model repo {repo}@{revision}"))
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202pub struct HfModelIdentity {
203 pub model_id: String,
204 pub repo_id: String,
205 pub revision: String,
206 pub file: String,
207 pub canonical_ref: String,
208 pub distribution_id: Option<String>,
209 pub selector: Option<String>,
210}
211
212impl HfModelIdentity {
213 pub fn to_model_identity(&self) -> ModelIdentity {
214 ModelIdentity {
215 model_id: self.model_id.clone(),
216 source_repo: Some(self.repo_id.clone()),
217 source_revision: Some(self.revision.clone()),
218 source_file: Some(self.file.clone()),
219 canonical_ref: Some(self.canonical_ref.clone()),
220 distribution_id: self.distribution_id.clone(),
221 selector: self.selector.clone(),
222 }
223 }
224
225 pub fn distribution_ref(&self) -> Option<String> {
226 self.distribution_id.as_ref().map(|distribution_id| {
227 format!("{}@{}/{}", self.repo_id, self.revision, distribution_id)
228 })
229 }
230}
231
232pub fn hf_token_override() -> Option<String> {
233 for key in ["HF_TOKEN", "HUGGING_FACE_HUB_TOKEN"] {
234 if let Ok(token) = std::env::var(key) {
235 let token = token.trim();
236 if !token.is_empty() {
237 return Some(token.to_string());
238 }
239 }
240 }
241 None
242}
243
244pub fn huggingface_repo_folder_name(repo_id: &str, repo_type: impl RepoType) -> String {
245 let type_plural = repo_type.plural();
246 std::iter::once(type_plural)
247 .chain(repo_id.split('/'))
248 .collect::<Vec<_>>()
249 .join("--")
250}
251
252pub fn huggingface_snapshot_path(
253 repo_id: &str,
254 repo_type: impl RepoType,
255 revision: &str,
256) -> PathBuf {
257 huggingface_hub_cache_dir()
258 .join(huggingface_repo_folder_name(repo_id, repo_type))
259 .join("snapshots")
260 .join(revision)
261}
262
263pub fn huggingface_identity_for_path_in_cache(
264 path: &Path,
265 cache_root: &Path,
266) -> Option<HfModelIdentity> {
267 if let Some(identity) = identity_from_cache_snapshot_path(path, cache_root) {
268 return Some(identity);
269 }
270 let resolved_cache_root = cache_root
271 .canonicalize()
272 .unwrap_or_else(|_| cache_root.to_path_buf());
273 if resolved_cache_root != cache_root
274 && let Some(identity) = identity_from_cache_snapshot_path(path, &resolved_cache_root)
275 {
276 return Some(identity);
277 }
278 let resolved = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
279 if resolved != path {
280 if let Some(identity) = identity_from_cache_snapshot_path(&resolved, cache_root) {
281 return Some(identity);
282 }
283 if resolved_cache_root != cache_root
284 && let Some(identity) =
285 identity_from_cache_snapshot_path(&resolved, &resolved_cache_root)
286 {
287 return Some(identity);
288 }
289 }
290 if let Some(identity) = identity_from_snapshot_layout_ancestors(path) {
291 return Some(identity);
292 }
293 if resolved != path
294 && let Some(identity) = identity_from_snapshot_layout_ancestors(&resolved)
295 {
296 return Some(identity);
297 }
298 scan_hf_cache_identity_for_path(path, cache_root)
299}
300
301fn identity_from_cache_snapshot_path(path: &Path, cache_root: &Path) -> Option<HfModelIdentity> {
302 let relative = path.strip_prefix(cache_root).ok()?;
303 let mut components = relative.components();
304 let repo_folder = components.next()?.as_os_str().to_str()?;
305 let repo_id = parse_model_repo_folder_name(repo_folder)?;
306 if components.next()?.as_os_str() != OsStr::new("snapshots") {
307 return None;
308 }
309 let revision = components.next()?.as_os_str().to_str()?.to_string();
310 let file = components
311 .map(|component| component.as_os_str().to_str())
312 .collect::<Option<Vec<_>>>()?
313 .join("/");
314 if file.is_empty() {
315 return None;
316 }
317 Some(identity_from_parts(repo_id, revision, file))
318}
319
320fn identity_from_snapshot_layout_ancestors(path: &Path) -> Option<HfModelIdentity> {
321 for revision_dir in path.ancestors() {
322 let Some(snapshots_dir) = revision_dir.parent() else {
323 continue;
324 };
325 if snapshots_dir.file_name()? != OsStr::new("snapshots") {
326 continue;
327 }
328 let repo_dir = snapshots_dir.parent()?;
329 let repo_folder = repo_dir.file_name()?.to_str()?;
330 let repo_id = parse_model_repo_folder_name(repo_folder)?;
331 let revision = revision_dir.file_name()?.to_str()?.to_string();
332 let file = path
333 .strip_prefix(revision_dir)
334 .ok()?
335 .components()
336 .map(|component| component.as_os_str().to_str())
337 .collect::<Option<Vec<_>>>()?
338 .join("/");
339 if file.is_empty() {
340 continue;
341 }
342 return Some(identity_from_parts(repo_id, revision, file));
343 }
344 None
345}
346
347fn scan_hf_cache_identity_for_path(path: &Path, cache_root: &Path) -> Option<HfModelIdentity> {
348 let cache_info = scan_hf_cache_info(cache_root)?;
349 let resolved = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
350
351 for repo in &cache_info.repos {
352 let Some(repo_id) = cache_repo_id(repo) else {
353 continue;
354 };
355 for revision in &repo.revisions {
356 for file in &revision.files {
357 let candidate = file
358 .file_path
359 .canonicalize()
360 .unwrap_or_else(|_| file.file_path.clone());
361 if file.file_path != path && candidate != resolved {
362 continue;
363 }
364
365 let relative_path = file
366 .file_path
367 .strip_prefix(&revision.snapshot_path)
368 .ok()?
369 .to_string_lossy()
370 .replace('\\', "/");
371 if relative_path.is_empty() {
372 return None;
373 }
374
375 return Some(identity_from_parts(
376 repo_id.to_string(),
377 revision.commit_hash.clone(),
378 relative_path,
379 ));
380 }
381 }
382 }
383 None
384}
385
386fn scan_hf_cache_info(cache_root: &Path) -> Option<HFCacheInfo> {
387 let cache_root = cache_root.to_path_buf();
388 let scan = move || {
389 let runtime = tokio::runtime::Builder::new_current_thread()
390 .enable_all()
391 .build()
392 .ok()?;
393 runtime
394 .block_on(
395 HFClientBuilder::new()
396 .cache_dir(cache_root)
397 .build()
398 .ok()?
399 .scan_cache()
400 .send(),
401 )
402 .ok()
403 };
404
405 if tokio::runtime::Handle::try_current().is_ok() {
406 std::thread::spawn(scan).join().ok().flatten()
407 } else {
408 scan()
409 }
410}
411
412fn identity_from_parts(repo_id: String, revision: String, file: String) -> HfModelIdentity {
413 let selector = quant_selector_from_gguf_file(&file);
414 let model_id = format_model_ref(&repo_id, None, selector.as_deref());
415 let distribution_id = normalize_gguf_distribution_id(&file);
416 let canonical_ref = format_canonical_ref(&repo_id, &revision, &file);
417 HfModelIdentity {
418 model_id,
419 repo_id,
420 revision,
421 file,
422 canonical_ref,
423 distribution_id,
424 selector,
425 }
426}
427
428fn cache_repo_id(repo: &CachedRepoInfo) -> Option<&str> {
429 (repo.repo_type == RepoTypeModel.singular()).then_some(repo.repo_id.as_str())
430}
431
432fn parse_model_repo_folder_name(folder: &str) -> Option<String> {
433 folder
434 .strip_prefix("models--")
435 .map(|value| value.replace("--", "/"))
436}
437
438fn repo_parts(repo: &str) -> (&str, &str) {
439 repo.split_once('/').unwrap_or(("", repo))
440}
441
442fn env_usize(key: &str) -> Option<usize> {
443 let value = std::env::var(key).ok()?;
444 value.trim().parse().ok()
445}
446
447fn env_duration_millis(key: &str) -> Option<Duration> {
448 env_usize(key).map(|millis| Duration::from_millis(millis as u64))
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use std::path::Path;
455 use std::sync::{
456 Arc, Mutex,
457 atomic::{AtomicUsize, Ordering},
458 };
459 use std::time::Duration;
460
461 #[test]
462 fn cache_path_identity_matches_mesh_snapshot_layout() {
463 let cache_root = PathBuf::from("/cache/hub");
464 let path = cache_root
465 .join("models--org--repo")
466 .join("snapshots")
467 .join("abc123")
468 .join("Qwen3-8B-Q4_K_M.gguf");
469
470 let identity = huggingface_identity_for_path_in_cache(&path, &cache_root).unwrap();
471 assert_eq!(identity.model_id, "org/repo:Q4_K_M");
472 assert_eq!(identity.repo_id, "org/repo");
473 assert_eq!(identity.revision, "abc123");
474 assert_eq!(identity.file, "Qwen3-8B-Q4_K_M.gguf");
475 assert_eq!(
476 identity.canonical_ref,
477 "org/repo@abc123/Qwen3-8B-Q4_K_M.gguf"
478 );
479 assert_eq!(identity.distribution_id.as_deref(), Some("Qwen3-8B-Q4_K_M"));
480 assert_eq!(
481 identity.distribution_ref().as_deref(),
482 Some("org/repo@abc123/Qwen3-8B-Q4_K_M")
483 );
484 }
485
486 #[test]
487 fn cache_path_identity_collapses_split_gguf_distribution() {
488 let cache_root = PathBuf::from("/cache/hub");
489 let path = cache_root
490 .join("models--org--repo")
491 .join("snapshots")
492 .join("abc123")
493 .join("UD-IQ2_M")
494 .join("GLM-5.1-UD-IQ2_M-00001-of-00006.gguf");
495
496 let identity = huggingface_identity_for_path_in_cache(&path, &cache_root).unwrap();
497 assert_eq!(identity.model_id, "org/repo:UD-IQ2_M");
498 assert_eq!(identity.selector.as_deref(), Some("UD-IQ2_M"));
499 assert_eq!(
500 identity.distribution_id.as_deref(),
501 Some("GLM-5.1-UD-IQ2_M")
502 );
503 }
504
505 #[test]
506 fn cache_path_identity_falls_back_to_snapshot_layout_ancestors() {
507 let path = PathBuf::from("/alternate/root")
508 .join("models--org--repo")
509 .join("snapshots")
510 .join("abc123")
511 .join("nested")
512 .join("Qwen3-8B-Q4_K_M.gguf");
513
514 let identity =
515 huggingface_identity_for_path_in_cache(&path, Path::new("/unrelated/cache")).unwrap();
516
517 assert_eq!(identity.model_id, "org/repo:Q4_K_M");
518 assert_eq!(identity.repo_id, "org/repo");
519 assert_eq!(identity.revision, "abc123");
520 assert_eq!(identity.file, "nested/Qwen3-8B-Q4_K_M.gguf");
521 }
522
523 #[test]
524 fn repo_folder_name_matches_huggingface_cache_layout() {
525 assert_eq!(
526 huggingface_repo_folder_name("org/repo", RepoTypeModel),
527 "models--org--repo"
528 );
529 }
530
531 #[tokio::test]
532 async fn download_file_resumes_existing_incomplete_cache_blob() {
533 let body = Arc::new(b"abcdefghij".to_vec());
534 let ranges = Arc::new(Mutex::new(Vec::new()));
535 let endpoint = start_http_resume_server(Arc::clone(&body), Arc::clone(&ranges));
536
537 let cache_dir = tempfile::tempdir().unwrap();
538 let incomplete = cache_dir
539 .path()
540 .join("models--owner--repo")
541 .join("blobs")
542 .join(format!("{TEST_ETAG}.incomplete"));
543 std::fs::create_dir_all(incomplete.parent().unwrap()).unwrap();
544 std::fs::write(&incomplete, b"abcd").unwrap();
545
546 let repo = HfModelRepository::builder()
547 .endpoint(endpoint)
548 .cache_dir(cache_dir.path())
549 .build()
550 .unwrap();
551
552 let path = repo
553 .download_file("owner/repo", "main", "model.bin")
554 .await
555 .unwrap();
556
557 assert_eq!(std::fs::read(path).unwrap(), body.as_slice());
558 assert!(
559 ranges
560 .lock()
561 .unwrap()
562 .iter()
563 .any(|range| range == "bytes=4-")
564 );
565 }
566
567 #[tokio::test]
568 async fn retry_config_recovers_rate_limited_repo_info() {
569 let endpoint = start_rate_limited_repo_info_server(1);
570 let cache_dir = tempfile::tempdir().unwrap();
571 let repo = HfModelRepository::builder()
572 .endpoint(endpoint)
573 .cache_dir(cache_dir.path())
574 .retry_max_attempts(1)
575 .retry_base_delay(Duration::from_millis(1))
576 .build()
577 .unwrap();
578
579 let revision = repo
580 .resolve_revision("owner/repo", Some("main"))
581 .await
582 .unwrap();
583
584 assert_eq!(revision, TEST_COMMIT);
585 }
586
587 #[tokio::test]
588 async fn retry_config_can_disable_rate_limited_retry() {
589 let endpoint = start_rate_limited_repo_info_server(1);
590 let cache_dir = tempfile::tempdir().unwrap();
591 let repo = HfModelRepository::builder()
592 .endpoint(endpoint)
593 .cache_dir(cache_dir.path())
594 .retry_max_attempts(0)
595 .retry_base_delay(Duration::from_millis(1))
596 .build()
597 .unwrap();
598
599 let error = repo
600 .resolve_revision("owner/repo", Some("main"))
601 .await
602 .unwrap_err();
603
604 assert!(
605 error.to_string().contains("fetch Hugging Face model repo"),
606 "unexpected error: {error:?}"
607 );
608 assert!(
609 format!("{error:?}").contains("Rate limited"),
610 "unexpected error chain: {error:?}"
611 );
612 }
613
614 const TEST_COMMIT: &str = "0123456789012345678901234567890123456789";
615 const TEST_ETAG: &str = "etag-http";
616
617 fn start_http_resume_server(body: Arc<Vec<u8>>, ranges: Arc<Mutex<Vec<String>>>) -> String {
618 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
619 let addr = listener.local_addr().unwrap();
620 std::thread::spawn(move || {
621 for connection in listener.incoming() {
622 let Ok(mut stream) = connection else {
623 return;
624 };
625 let body = Arc::clone(&body);
626 let ranges = Arc::clone(&ranges);
627 std::thread::spawn(move || handle_resume_request(&mut stream, &body, &ranges));
628 }
629 });
630 format!("http://{addr}")
631 }
632
633 fn start_rate_limited_repo_info_server(rate_limited_attempts: usize) -> String {
634 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
635 let addr = listener.local_addr().unwrap();
636 let attempts = Arc::new(AtomicUsize::new(0));
637 std::thread::spawn(move || {
638 for connection in listener.incoming() {
639 let Ok(mut stream) = connection else {
640 return;
641 };
642 let attempts = Arc::clone(&attempts);
643 std::thread::spawn(move || {
644 handle_rate_limited_repo_info_request(
645 &mut stream,
646 &attempts,
647 rate_limited_attempts,
648 )
649 });
650 }
651 });
652 format!("http://{addr}")
653 }
654
655 fn handle_resume_request(
656 stream: &mut std::net::TcpStream,
657 body: &[u8],
658 ranges: &Mutex<Vec<String>>,
659 ) {
660 use std::io::{Read, Write};
661
662 let mut request = vec![0; 4096];
663 let Ok(read) = stream.read(&mut request) else {
664 return;
665 };
666 let request = String::from_utf8_lossy(&request[..read]);
667 let is_head = request.starts_with("HEAD ");
668 let range = request.lines().find_map(range_header_value);
669 if !is_head && let Some(range) = range {
670 ranges.lock().unwrap().push(range.to_string());
671 }
672 let response = http_resume_response(body, is_head, range);
673 let _ = stream.write_all(&response);
674 }
675
676 fn handle_rate_limited_repo_info_request(
677 stream: &mut std::net::TcpStream,
678 attempts: &AtomicUsize,
679 rate_limited_attempts: usize,
680 ) {
681 use std::io::{Read, Write};
682
683 let mut request = vec![0; 4096];
684 let Ok(read) = stream.read(&mut request) else {
685 return;
686 };
687 let request = String::from_utf8_lossy(&request[..read]);
688 let path = request
689 .lines()
690 .next()
691 .and_then(|line| line.split_whitespace().nth(1))
692 .unwrap_or("/");
693 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
694 let response =
695 if path == "/api/models/owner/repo/revision/main" && attempt < rate_limited_attempts {
696 response_bytes_with_headers(
697 "429 Too Many Requests",
698 &[("Retry-After", "0")],
699 br#"{"error":"rate limited"}"#,
700 )
701 } else if path == "/api/models/owner/repo/revision/main" {
702 response_bytes_with_headers(
703 "200 OK",
704 &[("Content-Type", "application/json")],
705 format!(r#"{{"id":"owner/repo","sha":"{TEST_COMMIT}"}}"#).as_bytes(),
706 )
707 } else {
708 response_bytes_with_headers("404 Not Found", &[], br#"{"error":"not found"}"#)
709 };
710 let _ = stream.write_all(&response);
711 }
712
713 fn range_header_value(line: &str) -> Option<&str> {
714 let (name, value) = line.split_once(':')?;
715 name.eq_ignore_ascii_case("range").then(|| value.trim())
716 }
717
718 fn http_resume_response(body: &[u8], is_head: bool, range: Option<&str>) -> Vec<u8> {
719 if is_head {
720 return response_bytes("200 OK", body.len(), None, &[]);
721 }
722 if range == Some("bytes=4-") {
723 return response_bytes(
724 "206 Partial Content",
725 body.len() - 4,
726 Some("bytes 4-9/10"),
727 &body[4..],
728 );
729 }
730 response_bytes("200 OK", body.len(), None, body)
731 }
732
733 fn response_bytes(
734 status: &str,
735 content_length: usize,
736 content_range: Option<&str>,
737 body: &[u8],
738 ) -> Vec<u8> {
739 let content_range = content_range
740 .map(|value| format!("Content-Range: {value}\r\n"))
741 .unwrap_or_default();
742 format!(
743 "HTTP/1.1 {status}\r\n\
744 ETag: \"{TEST_ETAG}\"\r\n\
745 X-Repo-Commit: {TEST_COMMIT}\r\n\
746 Content-Length: {content_length}\r\n\
747 {content_range}\
748 Connection: close\r\n\
749 \r\n"
750 )
751 .into_bytes()
752 .into_iter()
753 .chain(body.iter().copied())
754 .collect()
755 }
756
757 fn response_bytes_with_headers(status: &str, headers: &[(&str, &str)], body: &[u8]) -> Vec<u8> {
758 let extra_headers = headers
759 .iter()
760 .map(|(name, value)| format!("{name}: {value}\r\n"))
761 .collect::<String>();
762 format!(
763 "HTTP/1.1 {status}\r\n\
764 Content-Length: {}\r\n\
765 {extra_headers}\
766 Connection: close\r\n\
767 \r\n",
768 body.len()
769 )
770 .into_bytes()
771 .into_iter()
772 .chain(body.iter().copied())
773 .collect()
774 }
775}