use crate::builder::BuilderError;
use crate::builder::http;
use reqwest::Client;
use std::path::Path;
fn find_sde_build_number(jsonl: &str) -> Option<String> {
for line in jsonl.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let Ok(record) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if record.get("_key").and_then(|v| v.as_str()) != Some("sde") {
continue;
}
let build = record.get("buildNumber")?;
return Some(match build {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
});
}
None
}
pub async fn update_as_needed(
client: &Client,
data_dir: &Path,
sde_url_base: &str,
variant: &str,
) -> Result<bool, BuilderError> {
std::fs::create_dir_all(data_dir)?;
let build_file = data_dir.join(format!("sde-{variant}.build"));
let zip_file = data_dir.join(format!("sde-{variant}.zip"));
let temp_zip_file = data_dir.join(format!("sde-{variant}.zip.tmp"));
let index_url = format!("{sde_url_base}latest.jsonl");
let index_contents = match http::fetch_text(client, &index_url).await {
Ok(contents) => contents,
Err(err) => {
eprintln!("sde_index: couldn't download {index_url} ({err})");
return Ok(false);
}
};
let Some(latest_build) = find_sde_build_number(&index_contents) else {
eprintln!("sde_index: couldn't determine the most recent build number in {index_url}");
return Ok(false);
};
let current_build = std::fs::read_to_string(&build_file)
.ok()
.map(|s| s.trim().to_string());
if current_build.as_deref() == Some(latest_build.as_str()) && zip_file.exists() {
println!("sde_index: {variant} data already up to date (build {latest_build})");
return Ok(false);
}
println!(
"sde_index: new build available ({} -> {latest_build}), downloading {variant} data",
current_build.as_deref().unwrap_or("none")
);
let zip_url = format!("{sde_url_base}eve-online-static-data-{latest_build}-{variant}.zip");
http::download(client, &zip_url, &temp_zip_file, |_| {}).await?;
std::fs::rename(&temp_zip_file, &zip_file)?;
std::fs::write(&build_file, &latest_build)?;
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_sde_build_number_extracts_matching_key() {
let jsonl = "{\"_key\": \"sde\", \"buildNumber\": 12345}\n";
assert_eq!(find_sde_build_number(jsonl), Some("12345".to_string()));
}
#[test]
fn find_sde_build_number_ignores_other_keys() {
let jsonl = concat!(
"{\"_key\": \"universe\", \"buildNumber\": 99999}\n",
"{\"_key\": \"sde\", \"buildNumber\": 12345}\n",
"{\"_key\": \"bsd\", \"buildNumber\": 11111}\n",
);
assert_eq!(find_sde_build_number(jsonl), Some("12345".to_string()));
}
#[test]
fn find_sde_build_number_returns_none_when_missing() {
let jsonl = "{\"_key\": \"universe\", \"buildNumber\": 99999}\n";
assert_eq!(find_sde_build_number(jsonl), None);
}
#[test]
fn find_sde_build_number_skips_malformed_lines_without_aborting() {
let jsonl = concat!(
"not valid json\n",
"\n", "{\"_key\": \"sde\", \"buildNumber\": 42}\n",
);
assert_eq!(find_sde_build_number(jsonl), Some("42".to_string()));
}
#[test]
fn find_sde_build_number_accepts_string_build_numbers() {
let jsonl = "{\"_key\": \"sde\", \"buildNumber\": \"12345\"}\n";
assert_eq!(find_sde_build_number(jsonl), Some("12345".to_string()));
}
#[test]
fn find_sde_build_number_handles_real_latest_jsonl() {
let jsonl = "{\"_key\": \"sde\", \"buildNumber\": 3458726, \"releaseDate\": \"2026-08-06T11:07:36Z\"}\r\n";
assert_eq!(find_sde_build_number(jsonl), Some("3458726".to_string()));
}
fn temp_data_dir(name: &str) -> std::path::PathBuf {
let dir =
std::env::temp_dir().join(format!("sde-index-test-{name}-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[tokio::test]
async fn update_as_needed_downloads_on_first_run() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/latest.jsonl"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_string("{\"_key\": \"sde\", \"buildNumber\": 123}\n"),
)
.mount(&server)
.await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path(
"/eve-online-static-data-123-jsonl.zip",
))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_bytes(b"zip content".to_vec()),
)
.mount(&server)
.await;
let client = http::build_client().unwrap();
let data_dir = temp_data_dir("first_run");
let base_url = format!("{}/", server.uri());
let changed = update_as_needed(&client, &data_dir, &base_url, "jsonl")
.await
.unwrap();
assert!(changed);
let build = std::fs::read_to_string(data_dir.join("sde-jsonl.build")).unwrap();
assert_eq!(build, "123");
let zip_contents = std::fs::read(data_dir.join("sde-jsonl.zip")).unwrap();
assert_eq!(zip_contents, b"zip content");
assert!(!data_dir.join("sde-jsonl.zip.tmp").exists());
}
#[tokio::test]
async fn update_as_needed_skips_when_build_matches() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/latest.jsonl"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_string("{\"_key\": \"sde\", \"buildNumber\": 123}\n"),
)
.mount(&server)
.await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path(
"/eve-online-static-data-123-jsonl.zip",
))
.respond_with(wiremock::ResponseTemplate::new(200))
.expect(0)
.mount(&server)
.await;
let client = http::build_client().unwrap();
let data_dir = temp_data_dir("matches");
std::fs::write(data_dir.join("sde-jsonl.build"), "123").unwrap();
std::fs::write(data_dir.join("sde-jsonl.zip"), b"previous zip").unwrap();
let base_url = format!("{}/", server.uri());
let changed = update_as_needed(&client, &data_dir, &base_url, "jsonl")
.await
.unwrap();
assert!(!changed);
let zip_contents = std::fs::read(data_dir.join("sde-jsonl.zip")).unwrap();
assert_eq!(zip_contents, b"previous zip");
}
#[tokio::test]
async fn update_as_needed_downloads_when_build_changed() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/latest.jsonl"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_string("{\"_key\": \"sde\", \"buildNumber\": 456}\n"),
)
.mount(&server)
.await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path(
"/eve-online-static-data-456-jsonl.zip",
))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_bytes(b"new zip".to_vec()))
.mount(&server)
.await;
let client = http::build_client().unwrap();
let data_dir = temp_data_dir("changed");
std::fs::write(data_dir.join("sde-jsonl.build"), "123").unwrap();
std::fs::write(data_dir.join("sde-jsonl.zip"), b"old zip").unwrap();
let base_url = format!("{}/", server.uri());
let changed = update_as_needed(&client, &data_dir, &base_url, "jsonl")
.await
.unwrap();
assert!(changed);
let build = std::fs::read_to_string(data_dir.join("sde-jsonl.build")).unwrap();
assert_eq!(build, "456");
let zip_contents = std::fs::read(data_dir.join("sde-jsonl.zip")).unwrap();
assert_eq!(zip_contents, b"new zip");
}
#[tokio::test]
async fn update_as_needed_returns_false_when_index_unreachable() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/latest.jsonl"))
.respond_with(wiremock::ResponseTemplate::new(500))
.mount(&server)
.await;
let client = http::build_client().unwrap();
let data_dir = temp_data_dir("unreachable");
let base_url = format!("{}/", server.uri());
let changed = update_as_needed(&client, &data_dir, &base_url, "jsonl")
.await
.unwrap();
assert!(!changed);
}
}