1use crate::error::{Error, Result};
14use std::fs;
15use std::io::Read;
16use std::path::{Path, PathBuf};
17
18const RELEASE_BASE: &str = "https://github.com/interscript/maps/releases/download";
19
20fn asset_url(version: &str, name: &str) -> String {
21 format!("{RELEASE_BASE}/v{version}/{name}")
22}
23
24fn sha256_file(path: &Path) -> Result<String> {
26 use sha2::{Digest, Sha256};
27 let mut file = fs::File::open(path)?;
28 let mut hasher = Sha256::new();
29 let mut buf = [0u8; 65_536];
30 loop {
31 let n = file.read(&mut buf)?;
32 if n == 0 {
33 break;
34 }
35 hasher.update(&buf[..n]);
36 }
37 Ok(hasher.finalize().iter().map(|b| format!("{b:02x}")).collect())
38}
39
40fn download(url: &str, dest: &Path) -> Result<()> {
42 let response = ureq::get(url).call().map_err(|e| {
43 Error::MapLogic(format!("download {url}: {e}"))
44 })?;
45 let mut reader = response.into_body().into_reader();
46 let mut file = fs::File::create(dest)?;
47 std::io::copy(&mut reader, &mut file)?;
48 Ok(())
49}
50
51pub fn fetch_corpus(version: &str, dest: &Path) -> Result<PathBuf> {
56 let name = format!("interscript-maps-isc-{version}.tar.gz");
57 fs::create_dir_all(dest)?;
58
59 let archive_path = dest.join(&name);
60 if !archive_path.exists() {
61 download(&asset_url(version, &name), &archive_path)?;
62 }
63
64 let sidecar_path = dest.join(format!("{name}.sha256"));
66 if !sidecar_path.exists() {
67 download(&asset_url(version, &format!("{name}.sha256")), &sidecar_path)?;
68 }
69 let expected = fs::read_to_string(&sidecar_path)?
70 .split_whitespace()
71 .next()
72 .ok_or_else(|| Error::MapLogic(format!("{sidecar_path:?}: empty sidecar")))?
73 .to_string();
74 let actual = sha256_file(&archive_path)?;
75 if actual != expected {
76 return Err(Error::MapLogic(format!(
77 "{name}: sha256 mismatch (expected {expected}, got {actual})"
78 )));
79 }
80
81 let gz = fs::File::open(&archive_path)?;
83 let gz = flate2::read::GzDecoder::new(gz);
84 let mut archive = tar::Archive::new(gz);
85 archive.unpack(dest).map_err(|e| {
86 Error::MapLogic(format!("unpack {name}: {e}"))
87 })?;
88
89 let maps_dir = dest.join("maps");
90 if !maps_dir.is_dir() {
91 return Err(Error::MapLogic(format!(
92 "{name}: unpacked but no maps/ directory found"
93 )));
94 }
95 Ok(maps_dir)
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn sha_mismatch_is_rejected() {
104 let dir = std::env::temp_dir().join(format!(
105 "iscrs-fetch-test-{}",
106 std::process::id()
107 ));
108 fs::create_dir_all(&dir).unwrap();
109 let name = "interscript-maps-isc-9.9.9.tar.gz";
110 let archive = dir.join(name);
111 fs::write(&archive, b"not a real archive").unwrap();
112 fs::write(dir.join(format!("{name}.sha256")), format!("{} {name}\n", "0".repeat(64)))
113 .unwrap();
114
115 match fetch_corpus("9.9.9", &dir) {
116 Err(Error::MapLogic(msg)) => {
117 assert!(msg.contains("sha256 mismatch"), "{msg}");
118 }
119 other => panic!("expected sha256 mismatch error, got {other:?}"),
120 }
121 fs::remove_dir_all(&dir).ok();
122 }
123}