use blake2::{digest::consts::U5, Blake2b, Digest};
use std::fs;
use std::hash::Hash;
use std::io;
use std::panic::catch_unwind;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use thiserror::Error;
use wasmer::{DeserializeError, Module, Target};
use cosmwasm_std::Checksum;
use crate::errors::{VmError, VmResult};
use crate::filesystem::mkdir_p;
use crate::modules::current_wasmer_module_version;
use crate::wasm_backend::make_runtime_engine;
use crate::wasm_backend::COST_FUNCTION_HASH;
use crate::Size;
use super::cached_module::engine_size_estimate;
use super::CachedModule;
const MODULE_SERIALIZATION_VERSION: &str = "v20";
#[inline]
fn raw_module_version_discriminator() -> String {
let hashes = [COST_FUNCTION_HASH];
let mut hasher = Blake2b::<U5>::new();
hasher.update(MODULE_SERIALIZATION_VERSION.as_bytes());
hasher.update(wasmer::VERSION.as_bytes());
for hash in hashes {
hasher.update(hash);
}
hex::encode(hasher.finalize())
}
#[inline]
fn module_version_discriminator() -> &'static str {
static DISCRIMINATOR: OnceLock<String> = OnceLock::new();
DISCRIMINATOR.get_or_init(raw_module_version_discriminator)
}
pub struct FileSystemCache {
modules_path: PathBuf,
unchecked_modules: bool,
}
#[derive(Error, Debug)]
pub enum NewFileSystemCacheError {
#[error("Could not get metadata of cache path")]
CouldntGetMetadata,
#[error("The supplied path is readonly")]
ReadonlyPath,
#[error("The supplied path already exists but is no directory")]
ExistsButNoDirectory,
#[error("Could not create cache path")]
CouldntCreatePath,
}
impl FileSystemCache {
pub unsafe fn new(
base_path: impl Into<PathBuf>,
unchecked_modules: bool,
) -> Result<Self, NewFileSystemCacheError> {
let base_path: PathBuf = base_path.into();
if base_path.exists() {
let metadata = base_path
.metadata()
.map_err(|_e| NewFileSystemCacheError::CouldntGetMetadata)?;
if !metadata.is_dir() {
return Err(NewFileSystemCacheError::ExistsButNoDirectory);
}
if metadata.permissions().readonly() {
return Err(NewFileSystemCacheError::ReadonlyPath);
}
} else {
mkdir_p(&base_path).map_err(|_e| NewFileSystemCacheError::CouldntCreatePath)?;
}
Ok(Self {
modules_path: modules_path(
&base_path,
current_wasmer_module_version(),
&Target::default(),
),
unchecked_modules,
})
}
pub fn set_module_unchecked(&mut self, unchecked: bool) {
self.unchecked_modules = unchecked;
}
fn module_file(&self, checksum: &Checksum) -> PathBuf {
let mut path = self.modules_path.clone();
path.push(checksum.to_hex());
path.set_extension("module");
path
}
pub fn load(
&self,
checksum: &Checksum,
memory_limit: Option<Size>,
) -> VmResult<Option<CachedModule>> {
let file_path = self.module_file(checksum);
let engine = make_runtime_engine(memory_limit);
let result = if self.unchecked_modules {
unsafe { Module::deserialize_from_file_unchecked(&engine, &file_path) }
} else {
unsafe { Module::deserialize_from_file(&engine, &file_path) }
};
match result {
Ok(module) => {
let module_size = module_size(&file_path)?;
Ok(Some(CachedModule {
module,
engine,
size_estimate: module_size + engine_size_estimate(),
}))
}
Err(DeserializeError::Io(err)) => match err.kind() {
io::ErrorKind::NotFound => Ok(None),
_ => Err(VmError::cache_err(format!(
"Error opening module file: {err}"
))),
},
Err(err) => Err(VmError::cache_err(format!(
"Error deserializing module: {err}"
))),
}
}
pub fn store(&mut self, checksum: &Checksum, module: &Module) -> VmResult<usize> {
mkdir_p(&self.modules_path)
.map_err(|_e| VmError::cache_err("Error creating modules directory"))?;
let path = self.module_file(checksum);
catch_unwind(|| {
module
.serialize_to_file(&path)
.map_err(|e| VmError::cache_err(format!("Error writing module to disk: {e}")))
})
.map_err(|_| VmError::cache_err("Could not write module to disk"))??;
let module_size = module_size(&path)?;
Ok(module_size)
}
pub fn remove(&mut self, checksum: &Checksum) -> VmResult<bool> {
let file_path = self.module_file(checksum);
if file_path.exists() {
fs::remove_file(file_path)
.map_err(|_e| VmError::cache_err("Error deleting module from disk"))?;
Ok(true)
} else {
Ok(false)
}
}
}
fn module_size(module_path: &Path) -> VmResult<usize> {
let module_size: usize = module_path
.metadata()
.map_err(|_e| VmError::cache_err("Error getting file metadata"))? .len()
.try_into()
.expect("Could not convert file size to usize");
Ok(module_size)
}
fn target_id(target: &Target) -> String {
let mut deterministic_hasher = crc32fast::Hasher::new();
target.hash(&mut deterministic_hasher);
let hash = deterministic_hasher.finalize();
format!("{}-{:08X}", target.triple(), hash) }
fn modules_path(base_path: &Path, wasmer_module_version: u32, target: &Target) -> PathBuf {
let version_dir = format!(
"{}-wasmer{wasmer_module_version}",
module_version_discriminator()
);
let target_dir = target_id(target);
base_path.join(version_dir).join(target_dir)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wasm_backend::{compile, make_compiling_engine};
use tempfile::TempDir;
use wasmer::{imports, Instance as WasmerInstance, Store};
use wasmer_middlewares::metering::set_remaining_points;
const TESTING_MEMORY_LIMIT: Option<Size> = Some(Size::mebi(16));
const TESTING_GAS_LIMIT: u64 = 500_000;
const SOME_WAT: &str = r#"(module
(type $t0 (func (param i32) (result i32)))
(func $add_one (export "add_one") (type $t0) (param $p0 i32) (result i32)
local.get $p0
i32.const 1
i32.add))
"#;
#[test]
fn file_system_cache_run() {
let tmp_dir = TempDir::new().unwrap();
let mut cache = unsafe { FileSystemCache::new(tmp_dir.path(), false).unwrap() };
let wasm = wat::parse_str(SOME_WAT).unwrap();
let checksum = Checksum::generate(&wasm);
let cached = cache.load(&checksum, TESTING_MEMORY_LIMIT).unwrap();
assert!(cached.is_none());
let compiling_engine = make_compiling_engine(TESTING_MEMORY_LIMIT);
let module = compile(&compiling_engine, &wasm).unwrap();
cache.store(&checksum, &module).unwrap();
let cached = cache.load(&checksum, TESTING_MEMORY_LIMIT).unwrap();
assert!(cached.is_some());
{
let CachedModule {
module: cached_module,
engine: runtime_engine,
size_estimate,
} = cached.unwrap();
assert_eq!(
size_estimate,
module.serialize().unwrap().len() + 10240
);
let import_object = imports! {};
let mut store = Store::new(runtime_engine);
let instance = WasmerInstance::new(&mut store, &cached_module, &import_object).unwrap();
set_remaining_points(&mut store, &instance, TESTING_GAS_LIMIT);
let add_one = instance.exports.get_function("add_one").unwrap();
let result = add_one.call(&mut store, &[42.into()]).unwrap();
assert_eq!(result[0].unwrap_i32(), 43);
}
}
#[test]
fn file_system_cache_store_uses_expected_path() {
let tmp_dir = TempDir::new().unwrap();
let mut cache = unsafe { FileSystemCache::new(tmp_dir.path(), false).unwrap() };
let wasm = wat::parse_str(SOME_WAT).unwrap();
let checksum = Checksum::generate(&wasm);
let engine = make_compiling_engine(TESTING_MEMORY_LIMIT);
let module = compile(&engine, &wasm).unwrap();
cache.store(&checksum, &module).unwrap();
let discriminator = raw_module_version_discriminator();
let mut globber = glob::glob(&format!(
"{}/{}-wasmer7/**/{}.module",
tmp_dir.path().to_string_lossy(),
discriminator,
checksum
))
.expect("Failed to read glob pattern");
let file_path = globber.next().unwrap().unwrap();
let _serialized_module = fs::read(file_path).unwrap();
}
#[test]
fn file_system_cache_remove_works() {
let tmp_dir = TempDir::new().unwrap();
let mut cache = unsafe { FileSystemCache::new(tmp_dir.path(), false).unwrap() };
let wasm = wat::parse_str(SOME_WAT).unwrap();
let checksum = Checksum::generate(&wasm);
let compiling_engine = make_compiling_engine(TESTING_MEMORY_LIMIT);
let module = compile(&compiling_engine, &wasm).unwrap();
cache.store(&checksum, &module).unwrap();
assert!(cache
.load(&checksum, TESTING_MEMORY_LIMIT)
.unwrap()
.is_some());
let existed = cache.remove(&checksum).unwrap();
assert!(existed);
assert!(cache
.load(&checksum, TESTING_MEMORY_LIMIT)
.unwrap()
.is_none());
let existed = cache.remove(&checksum).unwrap();
assert!(!existed);
}
#[test]
fn target_id_works() {
let triple = wasmer::Triple {
architecture: wasmer::Architecture::X86_64,
vendor: target_lexicon::Vendor::Nintendo,
operating_system: target_lexicon::OperatingSystem::Fuchsia,
environment: target_lexicon::Environment::Gnu,
binary_format: target_lexicon::BinaryFormat::Coff,
};
let target = Target::new(triple.clone(), wasmer::CpuFeature::POPCNT.into());
let id = target_id(&target);
assert_eq!(id, "x86_64-nintendo-fuchsia-gnu-coff-01E9F9FE");
let target = Target::new(triple, wasmer::CpuFeature::AVX512DQ.into());
let id = target_id(&target);
assert_eq!(id, "x86_64-nintendo-fuchsia-gnu-coff-93001945");
let target = Target::default();
let id1 = target_id(&target);
let id2 = target_id(&target);
assert_eq!(id1, id2);
}
#[test]
fn modules_path_works() {
let base = PathBuf::from("modules");
let triple = wasmer::Triple {
architecture: wasmer::Architecture::X86_64,
vendor: target_lexicon::Vendor::Nintendo,
operating_system: target_lexicon::OperatingSystem::Fuchsia,
environment: target_lexicon::Environment::Gnu,
binary_format: target_lexicon::BinaryFormat::Coff,
};
let target = Target::new(triple, wasmer::CpuFeature::POPCNT.into());
let p = modules_path(&base, 17, &target);
let discriminator = raw_module_version_discriminator();
assert_eq!(
p.as_os_str(),
if cfg!(windows) {
format!(
"modules\\{discriminator}-wasmer17\\x86_64-nintendo-fuchsia-gnu-coff-01E9F9FE"
)
} else {
format!(
"modules/{discriminator}-wasmer17/x86_64-nintendo-fuchsia-gnu-coff-01E9F9FE"
)
}
.as_str()
);
}
#[test]
fn module_version_discriminator_stays_the_same() {
let v1 = raw_module_version_discriminator();
let v2 = raw_module_version_discriminator();
let v3 = raw_module_version_discriminator();
let v4 = raw_module_version_discriminator();
assert_eq!(v1, v2);
assert_eq!(v2, v3);
assert_eq!(v3, v4);
}
#[test]
fn module_version_static() {
let version = raw_module_version_discriminator();
assert_eq!(version, "5b35f8ce52");
}
}