use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const WASMTIME_VERSION: &str = "27";
pub fn target_triple() -> &'static str {
option_env!("TARGET").unwrap_or("host")
}
pub const ENGINE_FINGERPRINT: &str =
"v2;component_model=true;async=false;fuel=false;epoch=true;cranelift";
pub fn engine_config_hash(fingerprint: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(fingerprint.as_bytes());
hex::encode(hasher.finalize())
}
pub fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheKey {
pub wasm_sha256: String,
pub wasmtime_version: String,
pub target_triple: String,
pub engine_config_hash: String,
}
impl CacheKey {
pub fn for_precompile(wasm_sha256: impl Into<String>) -> Self {
CacheKey {
wasm_sha256: wasm_sha256.into(),
wasmtime_version: WASMTIME_VERSION.to_string(),
target_triple: target_triple().to_string(),
engine_config_hash: engine_config_hash(ENGINE_FINGERPRINT),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarMeta {
pub schema: u32,
pub key: CacheKey,
}
impl SidecarMeta {
pub const SCHEMA_VERSION: u32 = 1;
pub fn new(key: CacheKey) -> Self {
SidecarMeta {
schema: Self::SCHEMA_VERSION,
key,
}
}
pub fn write_to(&self, path: &Path) -> Result<(), String> {
let s = toml::to_string(self)
.map_err(|e| format!("serialize cwasm sidecar {}: {}", path.display(), e))?;
std::fs::write(path, s)
.map_err(|e| format!("write cwasm sidecar {}: {}", path.display(), e))
}
}
pub fn make_engine() -> Result<wasmtime::Engine, String> {
let mut config = wasmtime::Config::new();
config.wasm_component_model(true);
config.async_support(false);
config.consume_fuel(false);
config.epoch_interruption(true);
wasmtime::Engine::new(&config).map_err(|e| format!("wasmtime Engine::new: {}", e))
}
#[derive(Debug, Clone)]
pub struct PrecompileOutput {
pub cwasm_path: PathBuf,
pub sidecar_path: PathBuf,
pub cache_key: CacheKey,
}
pub fn precompile(
wasm_path: &Path,
cache_dir: &Path,
engine: &wasmtime::Engine,
) -> Result<PrecompileOutput, String> {
let wasm_bytes =
std::fs::read(wasm_path).map_err(|e| format!("read {}: {}", wasm_path.display(), e))?;
let wasm_sha = sha256_hex(&wasm_bytes);
ensure_cache_dir(cache_dir)?;
let stem = wasm_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("invalid wasm filename: {}", wasm_path.display()))?;
let cwasm_path = cache_dir.join(format!("{}.cwasm", stem));
let sidecar_path = cache_dir.join(format!("{}.cwasm.meta", stem));
let serialized = engine
.precompile_component(&wasm_bytes)
.map_err(|e| format!("precompile_component {}: {}", wasm_path.display(), e))?;
write_with_mode(&cwasm_path, &serialized, 0o600)?;
let cache_key = CacheKey::for_precompile(wasm_sha);
SidecarMeta::new(cache_key.clone()).write_to(&sidecar_path)?;
set_mode(&sidecar_path, 0o600)?;
Ok(PrecompileOutput {
cwasm_path,
sidecar_path,
cache_key,
})
}
fn ensure_cache_dir(dir: &Path) -> Result<(), String> {
std::fs::create_dir_all(dir)
.map_err(|e| format!("create cache dir {}: {}", dir.display(), e))?;
set_mode(dir, 0o700)?;
Ok(())
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) -> Result<(), String> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.map_err(|e| format!("chmod {}: {}", path.display(), e))
}
#[cfg(not(unix))]
fn set_mode(_path: &Path, _mode: u32) -> Result<(), String> {
Ok(())
}
#[cfg(unix)]
fn write_with_mode(path: &Path, bytes: &[u8], mode: u32) -> Result<(), String> {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(mode)
.open(path)
.map_err(|e| format!("open {}: {}", path.display(), e))?;
f.write_all(bytes)
.map_err(|e| format!("write {}: {}", path.display(), e))?;
Ok(())
}
#[cfg(not(unix))]
fn write_with_mode(path: &Path, bytes: &[u8], _mode: u32) -> Result<(), String> {
std::fs::write(path, bytes).map_err(|e| format!("write {}: {}", path.display(), e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn engine_config_hash_is_deterministic() {
let a = engine_config_hash(ENGINE_FINGERPRINT);
let b = engine_config_hash(ENGINE_FINGERPRINT);
assert_eq!(a, b);
}
#[test]
fn engine_config_hash_differs_for_different_fingerprints() {
let a = engine_config_hash("a");
let b = engine_config_hash("b");
assert_ne!(a, b);
}
#[test]
fn cache_key_for_precompile_uses_pinned_constants() {
let k = CacheKey::for_precompile("abc");
assert_eq!(k.wasm_sha256, "abc");
assert_eq!(k.wasmtime_version, WASMTIME_VERSION);
assert_eq!(k.engine_config_hash, engine_config_hash(ENGINE_FINGERPRINT));
}
#[test]
fn sidecar_round_trip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("plugin.cwasm.meta");
let key = CacheKey::for_precompile("deadbeef");
let meta = SidecarMeta::new(key.clone());
meta.write_to(&path).unwrap();
let bytes = std::fs::read_to_string(&path).unwrap();
let parsed: SidecarMeta = toml::from_str(&bytes).unwrap();
assert_eq!(parsed.schema, SidecarMeta::SCHEMA_VERSION);
assert_eq!(parsed.key, key);
}
#[test]
fn make_engine_succeeds() {
let _engine = make_engine().expect("engine");
}
}