use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;
use anyhow::{bail, Context, Result};
#[derive(Clone, Copy, Debug)]
pub struct EmbeddedRuntime {
pub gz: &'static [u8],
pub crc32: u32,
pub size: u64,
pub file_name: &'static str,
pub version: &'static str,
}
static EMBEDDED: OnceLock<EmbeddedRuntime> = OnceLock::new();
pub fn set_embedded_runtime(runtime: EmbeddedRuntime) {
if !runtime.gz.is_empty() {
let _ = EMBEDDED.set(runtime);
}
}
pub(crate) fn embedded_runtime() -> Option<&'static EmbeddedRuntime> {
EMBEDDED.get()
}
pub(crate) fn cache_root(
nova_home: Option<PathBuf>,
userprofile: Option<PathBuf>,
home: Option<PathBuf>,
windows: bool,
) -> Result<PathBuf> {
match nova_pm::nova_home(nova_home, userprofile, home, windows) {
Some(home) => Ok(home.join("runtime")),
None => {
let name = if windows { "USERPROFILE" } else { "HOME" };
bail!(
"cannot place the runtime library's cache: neither NOVA_HOME nor {name} is \
set; set NOVA_HOME to a writable directory, or NOVA_RUNTIME_LIB to a runtime \
library"
)
}
}
}
pub(crate) fn unpack(runtime: &EmbeddedRuntime, root: &Path) -> Result<PathBuf> {
let dir = root.join(format!("{}-{:08x}", runtime.version, runtime.crc32));
let path = dir.join(runtime.file_name);
if verified(&path, runtime) {
return Ok(path);
}
fs::create_dir_all(&dir).with_context(|| {
format!(
"creating the runtime library's cache {}; set NOVA_HOME to a writable \
directory, or NOVA_RUNTIME_LIB to a runtime library",
dir.display()
)
})?;
let (file, temp) = new_temp_file(&dir, runtime.file_name)?;
if let Err(error) = decompress(runtime, file) {
let _ = fs::remove_file(&temp);
return Err(error.context(format!(
"unpacking the runtime library into {}",
temp.display()
)));
}
match fs::rename(&temp, &path) {
Ok(()) => Ok(path),
Err(error) => {
let _ = fs::remove_file(&temp);
if verified(&path, runtime) {
Ok(path)
} else {
Err(error)
.with_context(|| format!("moving the runtime library to {}", path.display()))
}
}
}
}
fn verified(path: &Path, runtime: &EmbeddedRuntime) -> bool {
let Ok(mut file) = fs::File::open(path) else {
return false;
};
match file.metadata() {
Ok(metadata) if metadata.len() == runtime.size => {}
_ => return false,
}
let mut hasher = crc32fast::Hasher::new();
let mut buffer = vec![0u8; 1 << 16];
loop {
match file.read(&mut buffer) {
Ok(0) => break,
Ok(read) => hasher.update(&buffer[..read]),
Err(_) => return false,
}
}
hasher.finalize() == runtime.crc32
}
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
fn new_temp_file(dir: &Path, file_name: &str) -> Result<(fs::File, PathBuf)> {
loop {
let n = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let temp = dir.join(format!("{file_name}.{}-{n}.tmp", std::process::id()));
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&temp)
{
Ok(file) => return Ok((file, temp)),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => {
return Err(error).with_context(|| format!("creating {}", temp.display()))
}
}
}
}
fn decompress(runtime: &EmbeddedRuntime, mut file: fs::File) -> Result<()> {
let mut decoder = flate2::read::GzDecoder::new(runtime.gz);
let mut hasher = crc32fast::Hasher::new();
let mut size = 0u64;
let mut buffer = vec![0u8; 1 << 16];
loop {
let read = decoder.read(&mut buffer).context("decompressing")?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
file.write_all(&buffer[..read]).context("writing")?;
size += read as u64;
}
file.flush().context("writing")?;
if size != runtime.size || hasher.finalize() != runtime.crc32 {
bail!(
"the runtime library this nova carries is damaged: it unpacked to {size} bytes \
with a different CRC-32, not the {} bytes it was built with",
runtime.size
);
}
Ok(())
}
pub(crate) fn locate(
override_path: Option<PathBuf>,
embedded: Option<&EmbeddedRuntime>,
cache_root: impl FnOnce() -> Result<PathBuf>,
beside_exe: impl FnOnce() -> Result<PathBuf>,
) -> Result<PathBuf> {
if let Some(path) = override_path {
if path.exists() {
return Ok(path);
}
bail!(
"NOVA_RUNTIME_LIB points to {}, which does not exist",
path.display()
);
}
match embedded {
Some(runtime) => unpack(runtime, &cache_root()?),
None => beside_exe(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_runtime() -> EmbeddedRuntime {
let library: Vec<u8> = (0..4 * 1024 * 1024u32).map(|i| (i % 251) as u8).collect();
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
encoder.write_all(&library).unwrap();
let gz = encoder.finish().unwrap();
EmbeddedRuntime {
gz: Box::leak(gz.into_boxed_slice()),
crc32: crc32fast::hash(&library),
size: library.len() as u64,
file_name: "fake_runtime.lib",
version: "0.0.0-test",
}
}
fn fresh_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("nova-runtime-cache-{name}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn library_path(root: &Path, runtime: &EmbeddedRuntime) -> PathBuf {
root.join(format!("0.0.0-test-{:08x}", runtime.crc32))
.join("fake_runtime.lib")
}
fn never() -> Result<PathBuf> {
panic!("this step of the lookup must not run")
}
#[test]
fn the_first_use_writes_the_library() {
let runtime = fake_runtime();
let root = fresh_dir("first");
let path = unpack(&runtime, &root).unwrap();
assert_eq!(path, library_path(&root, &runtime));
assert!(verified(&path, &runtime));
}
#[test]
fn a_second_use_reuses_the_file_untouched() {
let runtime = fake_runtime();
let root = fresh_dir("second");
let path = unpack(&runtime, &root).unwrap();
let written = fs::metadata(&path).unwrap().modified().unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
assert_eq!(unpack(&runtime, &root).unwrap(), path);
assert_eq!(fs::metadata(&path).unwrap().modified().unwrap(), written);
}
#[test]
fn a_damaged_copy_is_replaced() {
let runtime = fake_runtime();
let root = fresh_dir("damaged");
let path = unpack(&runtime, &root).unwrap();
fs::write(&path, vec![0u8; runtime.size as usize]).unwrap();
assert!(!verified(&path, &runtime));
unpack(&runtime, &root).unwrap();
assert!(verified(&path, &runtime));
fs::write(&path, b"short").unwrap();
unpack(&runtime, &root).unwrap();
assert!(verified(&path, &runtime));
}
#[test]
fn threads_unpacking_at_once_all_get_a_verified_file() {
let runtime = fake_runtime();
let root = fresh_dir("threads");
let barrier = std::sync::Barrier::new(8);
std::thread::scope(|scope| {
let handles: Vec<_> = (0..8)
.map(|_| {
scope.spawn(|| {
barrier.wait();
unpack(&runtime, &root)
})
})
.collect();
for handle in handles {
let path = handle.join().unwrap().unwrap();
assert!(verified(&path, &runtime));
}
});
let dir = library_path(&root, &runtime)
.parent()
.unwrap()
.to_path_buf();
let leftovers: Vec<_> = fs::read_dir(dir)
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.filter(|name| name.ends_with(".tmp"))
.collect();
assert!(leftovers.is_empty(), "{leftovers:?}");
}
#[test]
fn processes_unpacking_at_once_both_get_a_verified_file() {
let root = fresh_dir("processes");
let me = std::env::current_exe().unwrap();
let children: Vec<_> = (0..2)
.map(|_| {
std::process::Command::new(&me)
.args([
"--exact",
"runtime_cache::tests::unpack_as_a_child_process",
"--nocapture",
])
.env("NOVA_TEST_UNPACK_ROOT", &root)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap()
})
.collect();
for child in children {
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"a child failed:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let runtime = fake_runtime();
assert!(verified(&library_path(&root, &runtime), &runtime));
}
#[test]
fn unpack_as_a_child_process() {
let Some(root) = std::env::var_os("NOVA_TEST_UNPACK_ROOT") else {
return;
};
let runtime = fake_runtime();
let path = unpack(&runtime, Path::new(&root)).unwrap();
assert!(verified(&path, &runtime));
}
#[test]
fn a_nova_home_that_is_a_file_gives_an_error_naming_it() {
let runtime = fake_runtime();
let file = fresh_dir("home-is-a-file").join("not-a-directory");
fs::write(&file, b"").unwrap();
let error = format!("{:#}", unpack(&runtime, &file.join("runtime")).unwrap_err());
assert!(error.contains("not-a-directory"), "{error}");
assert!(error.contains("NOVA_HOME"), "{error}");
}
#[test]
fn a_cache_path_with_spaces_and_thai_letters_works() {
let runtime = fake_runtime();
let root = fresh_dir("path").join("โฟลเดอร์ มี ช่องว่าง");
let path = unpack(&runtime, &root).unwrap();
assert!(verified(&path, &runtime));
}
#[test]
fn nova_home_decides_the_root() {
let root = cache_root(
Some("D:/n".into()),
Some("C:/Users/u".into()),
Some("/home/u".into()),
true,
)
.unwrap();
assert_eq!(root, PathBuf::from("D:/n").join("runtime"));
}
#[test]
fn without_nova_home_the_root_is_under_the_home_directory() {
let windows = cache_root(
None,
Some("C:/Users/u".into()),
Some("/home/u".into()),
true,
);
assert_eq!(
windows.unwrap(),
PathBuf::from("C:/Users/u").join(".nova").join("runtime")
);
let unix = cache_root(
None,
Some("C:/Users/u".into()),
Some("/home/u".into()),
false,
);
assert_eq!(
unix.unwrap(),
PathBuf::from("/home/u").join(".nova").join("runtime")
);
}
#[test]
fn an_empty_variable_counts_as_unset() {
let root = cache_root(Some("".into()), Some("C:/Users/u".into()), None, true);
assert_eq!(
root.unwrap(),
PathBuf::from("C:/Users/u").join(".nova").join("runtime")
);
}
#[test]
fn with_no_home_at_all_the_error_names_the_variables() {
for windows in [true, false] {
let error = cache_root(None, None, None, windows)
.unwrap_err()
.to_string();
assert!(error.contains("NOVA_HOME"), "{error}");
let home = if windows { "USERPROFILE" } else { "HOME" };
assert!(error.contains(home), "{error}");
}
}
#[test]
fn nova_runtime_lib_comes_first() {
let runtime = fake_runtime();
let lib = fresh_dir("override").join("my_runtime.lib");
fs::write(&lib, b"").unwrap();
assert_eq!(
locate(Some(lib.clone()), Some(&runtime), never, never).unwrap(),
lib
);
}
#[test]
fn a_missing_nova_runtime_lib_is_an_error() {
let missing = PathBuf::from("no/such/runtime.lib");
let error = locate(Some(missing), None, never, never)
.unwrap_err()
.to_string();
assert!(error.contains("NOVA_RUNTIME_LIB"), "{error}");
}
#[test]
fn the_embedded_runtime_comes_before_the_executables_neighbours() {
let runtime = fake_runtime();
let root = fresh_dir("embedded-first");
let path = locate(None, Some(&runtime), || Ok(root.clone()), never).unwrap();
assert_eq!(path, library_path(&root, &runtime));
}
#[test]
fn an_embedded_runtime_that_fails_to_unpack_is_an_error_not_a_fall_back() {
let runtime = fake_runtime();
let error = locate(None, Some(&runtime), || bail!("no home here"), never)
.unwrap_err()
.to_string();
assert!(error.contains("no home here"), "{error}");
}
#[test]
fn with_nothing_embedded_the_neighbours_are_tried() {
let lib = PathBuf::from("beside/nova_runtime.lib");
assert_eq!(locate(None, None, never, || Ok(lib.clone())).unwrap(), lib);
}
}