use crate::utils::BraheError;
use crate::utils::atomic_write;
use crate::utils::cache::get_naif_cache_dir;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
const NAIF_BASE_URL: &str = "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/";
const SUPPORTED_KERNELS: &[&str] = &[
"de430", "de432s", "de435", "de438", "de440", "de440s", "de442", "de442s",
];
fn validate_kernel_name(name: &str) -> Result<(), BraheError> {
if SUPPORTED_KERNELS.contains(&name) {
Ok(())
} else {
Err(BraheError::Error(format!(
"Unsupported kernel name '{}'. Supported kernels: {}",
name,
SUPPORTED_KERNELS.join(", ")
)))
}
}
fn fetch_de_kernel_with_url(name: &str, base_url: &str) -> Result<Vec<u8>, BraheError> {
let filename = format!("{}.bsp", name);
let url = format!("{}{}", base_url, filename);
let response = ureq::get(&url).call().map_err(|e| {
BraheError::Error(format!(
"Failed to download kernel {} from NAIF: {}",
name, e
))
})?;
let mut buffer = Vec::new();
let mut reader = response.into_body().into_reader();
reader.read_to_end(&mut buffer).map_err(|e| {
BraheError::Error(format!(
"Failed to read kernel {} from NAIF response: {}",
name, e
))
})?;
if buffer.is_empty() {
return Err(BraheError::Error(format!(
"No data returned from NAIF for kernel '{}'",
name
)));
}
Ok(buffer)
}
fn fetch_de_kernel(name: &str) -> Result<Vec<u8>, BraheError> {
fetch_de_kernel_with_url(name, NAIF_BASE_URL)
}
pub fn download_de_kernel(name: &str, output_path: Option<PathBuf>) -> Result<PathBuf, BraheError> {
validate_kernel_name(name)?;
let cache_dir = get_naif_cache_dir()?;
let cache_path = PathBuf::from(&cache_dir).join(format!("{}.bsp", name));
if !cache_path.exists() {
let data = fetch_de_kernel(name)?;
atomic_write(&cache_path, &data).map_err(|e| {
BraheError::Error(format!(
"Failed to cache kernel {} to {}: {}",
name,
cache_path.display(),
e
))
})?;
}
if let Some(output) = output_path {
if let Some(parent) = output.parent() {
fs::create_dir_all(parent).map_err(|e| {
BraheError::Error(format!(
"Failed to create output directory {}: {}",
parent.display(),
e
))
})?;
}
fs::copy(&cache_path, &output).map_err(|e| {
BraheError::Error(format!(
"Failed to copy kernel from {} to {}: {}",
cache_path.display(),
output.display(),
e
))
})?;
Ok(output)
} else {
Ok(cache_path)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use httpmock::prelude::*;
use serial_test::serial;
use tempfile::tempdir;
fn setup_test_kernel() {
let test_asset_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test_assets")
.join("de440s.bsp");
if !test_asset_path.exists() {
return;
}
let cache_dir = get_naif_cache_dir().expect("Failed to get NAIF cache dir");
let cache_path = PathBuf::from(&cache_dir).join("de440s.bsp");
if !cache_path.exists() {
fs::copy(&test_asset_path, &cache_path).expect("Failed to copy test asset to cache");
}
}
#[test]
fn test_validate_kernel_name() {
assert!(validate_kernel_name("de430").is_ok());
assert!(validate_kernel_name("de432s").is_ok());
assert!(validate_kernel_name("de435").is_ok());
assert!(validate_kernel_name("de438").is_ok());
assert!(validate_kernel_name("de440").is_ok());
assert!(validate_kernel_name("de440s").is_ok());
assert!(validate_kernel_name("de442").is_ok());
assert!(validate_kernel_name("de442s").is_ok());
assert!(validate_kernel_name("de999").is_err());
assert!(validate_kernel_name("invalid").is_err());
assert!(validate_kernel_name("").is_err());
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
#[serial]
fn test_download_de_network() {
setup_test_kernel();
let result = download_de_kernel("de440s", None);
assert!(result.is_ok());
let kernel_path = result.unwrap();
assert!(kernel_path.exists());
assert!(kernel_path.to_string_lossy().contains("de440s.bsp"));
let metadata = fs::metadata(&kernel_path).unwrap();
assert!(metadata.len() > 0);
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
#[serial]
fn test_download_with_output_path_network() {
setup_test_kernel();
let temp_dir = std::env::temp_dir();
let output_path = temp_dir.join("test_de_output.bsp");
let _ = fs::remove_file(&output_path);
let result = download_de_kernel("de440s", Some(output_path.clone()));
assert!(result.is_ok());
let returned_path = result.unwrap();
assert_eq!(returned_path, output_path);
assert!(output_path.exists());
let metadata = fs::metadata(&output_path).unwrap();
assert!(metadata.len() > 0);
let _ = fs::remove_file(&output_path);
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
#[serial]
fn test_caching_behavior_network() {
setup_test_kernel();
let result1 = download_de_kernel("de440s", None);
assert!(result1.is_ok());
let kernel_path = result1.unwrap();
let metadata1 = fs::metadata(&kernel_path).unwrap();
let modified1 = metadata1.modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
let result2 = download_de_kernel("de440s", None);
assert!(result2.is_ok());
assert_eq!(kernel_path, result2.unwrap());
let metadata2 = fs::metadata(&kernel_path).unwrap();
let modified2 = metadata2.modified().unwrap();
assert_eq!(modified1, modified2);
}
#[test]
fn test_unsupported_kernel_name() {
let result = download_de_kernel("de999", None);
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BraheError::Error(msg) => {
assert!(msg.contains("Unsupported kernel name"));
assert!(msg.contains("de999"));
}
_ => panic!("Expected BraheError::Error"),
}
}
#[test]
fn test_supported_kernels_list() {
assert_eq!(SUPPORTED_KERNELS.len(), 8);
assert!(SUPPORTED_KERNELS.contains(&"de430"));
assert!(SUPPORTED_KERNELS.contains(&"de432s"));
assert!(SUPPORTED_KERNELS.contains(&"de435"));
assert!(SUPPORTED_KERNELS.contains(&"de438"));
assert!(SUPPORTED_KERNELS.contains(&"de440"));
assert!(SUPPORTED_KERNELS.contains(&"de440s"));
assert!(SUPPORTED_KERNELS.contains(&"de442"));
assert!(SUPPORTED_KERNELS.contains(&"de442s"));
}
#[test]
fn test_fetch_de_kernel_http_404() {
let server = MockServer::start();
let _mock = server.mock(|when, then| {
when.method(GET).path("/de440s.bsp");
then.status(404);
});
let result = fetch_de_kernel_with_url("de440s", &server.url("/"));
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BraheError::Error(msg) => {
assert!(msg.contains("Failed to download kernel"));
assert!(msg.contains("de440s"));
}
_ => panic!("Expected BraheError::Error"),
}
}
#[test]
fn test_fetch_de_kernel_http_500() {
let server = MockServer::start();
let _mock = server.mock(|when, then| {
when.method(GET).path("/de440s.bsp");
then.status(500);
});
let result = fetch_de_kernel_with_url("de440s", &server.url("/"));
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BraheError::Error(msg) => {
assert!(msg.contains("Failed to download kernel"));
assert!(msg.contains("de440s"));
}
_ => panic!("Expected BraheError::Error"),
}
}
#[test]
fn test_fetch_de_kernel_empty_response() {
let server = MockServer::start();
let _mock = server.mock(|when, then| {
when.method(GET).path("/de440s.bsp");
then.status(200).body("");
});
let result = fetch_de_kernel_with_url("de440s", &server.url("/"));
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BraheError::Error(msg) => {
assert!(msg.contains("No data returned"));
assert!(msg.contains("de440s"));
}
_ => panic!("Expected BraheError::Error"),
}
}
#[test]
fn test_download_output_is_directory() {
let temp_dir = tempdir().unwrap();
let output_path = temp_dir.path().join("some_directory");
fs::create_dir_all(&output_path).unwrap();
setup_test_kernel();
let result = download_de_kernel("de440s", Some(output_path.clone()));
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BraheError::Error(msg) => {
assert!(msg.contains("Failed to copy kernel"));
}
_ => panic!("Expected BraheError::Error"),
}
}
#[test]
#[cfg(unix)]
fn test_download_invalid_cache_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = tempdir().unwrap();
let cache_file = temp_dir.path().join("de440s.bsp");
fs::write(&cache_file, b"test").unwrap();
let mut perms = fs::metadata(&cache_file).unwrap().permissions();
perms.set_mode(0o444); fs::set_permissions(&cache_file, perms).unwrap();
let mut dir_perms = fs::metadata(temp_dir.path()).unwrap().permissions();
dir_perms.set_mode(0o555); fs::set_permissions(temp_dir.path(), dir_perms).unwrap();
let output_path = temp_dir.path().join("new_file.bsp");
setup_test_kernel();
let result = download_de_kernel("de440s", Some(output_path));
let mut dir_perms = fs::metadata(temp_dir.path()).unwrap().permissions();
dir_perms.set_mode(0o755);
fs::set_permissions(temp_dir.path(), dir_perms).unwrap();
assert!(result.is_err());
}
#[test]
fn test_download_output_copy_failure() {
let temp_dir = tempdir().unwrap();
let output_path = temp_dir.path().join("conflict");
fs::create_dir_all(&output_path).unwrap();
setup_test_kernel();
let result = download_de_kernel("de440s", Some(output_path));
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BraheError::Error(msg) => {
assert!(msg.contains("Failed to copy kernel"));
}
_ => panic!("Expected BraheError::Error"),
}
}
#[test]
#[cfg_attr(not(feature = "ci"), ignore)]
#[serial]
fn test_output_path_creates_directories() {
setup_test_kernel();
let temp_dir = tempdir().unwrap();
let nested_path = temp_dir
.path()
.join("level1")
.join("level2")
.join("level3")
.join("de440s.bsp");
assert!(!nested_path.parent().unwrap().exists());
let result = download_de_kernel("de440s", Some(nested_path.clone()));
assert!(result.is_ok());
assert!(nested_path.exists());
assert!(nested_path.parent().unwrap().exists());
let metadata = fs::metadata(&nested_path).unwrap();
assert!(metadata.len() > 0);
}
}