use std::path::PathBuf;
use std::time::SystemTime;
use anyhow::Result;
use serde::Deserialize;
use serde::Serialize;
use dprint_core::plugins::PluginInfo;
use super::implementations::WASM_CACHE_VERSION;
use crate::environment::Environment;
use crate::utils::FastInsecureHasher;
use crate::utils::PluginKind;
use std::hash::Hasher;
const PLUGIN_CACHE_SCHEMA_VERSION: usize = 10;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LocalStamp {
pub path: String,
pub len: u64,
pub modified_ms: u64,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PluginCacheMeta {
pub source: String,
pub signature: String,
pub plugin_kind: PluginKind,
pub created_time: u64,
pub info: PluginInfo,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub executable_sub_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub local_stamps: Option<Vec<LocalStamp>>,
}
impl PluginCacheMeta {
pub fn artifact_file_path(&self, hash: &str, environment: &impl Environment) -> PathBuf {
match self.plugin_kind {
PluginKind::Wasm => wasm_artifact_path(hash, environment),
PluginKind::Process => {
let sub_path = self.executable_sub_path.as_deref().unwrap_or_default();
process_dir_path(hash, environment).join(sub_path)
}
}
}
}
pub fn current_signature(environment: &impl Environment) -> String {
format!(
"{}-{}-{}-{}",
PLUGIN_CACHE_SCHEMA_VERSION,
WASM_CACHE_VERSION,
environment.wasm_cache_key(),
environment.os(),
)
}
pub fn entry_hash(cache_key: &str, environment: &impl Environment) -> String {
let mut hasher = FastInsecureHasher::default();
hasher.write(current_signature(environment).as_bytes());
hasher.write(&[0]);
hasher.write(cache_key.as_bytes());
format!("{:016x}", hasher.finish())
}
pub fn read_meta(hash: &str, environment: &impl Environment) -> Option<PluginCacheMeta> {
let text = environment.read_file(meta_path(hash, environment)).ok()?;
serde_json::from_str(&text).ok()
}
pub fn write_meta(hash: &str, meta: &PluginCacheMeta, environment: &impl Environment) -> Result<()> {
let serialized = serde_json::to_string(meta)?;
Ok(environment.atomic_write_file_bytes(meta_path(hash, environment), serialized.as_bytes())?)
}
pub fn remove_entry(hash: &str, environment: &impl Environment) {
let _ = environment.remove_file(meta_path(hash, environment));
let _ = environment.remove_file(wasm_artifact_path(hash, environment));
environment.try_remove_dir_all(process_dir_path(hash, environment));
}
pub fn to_unix_millis(time: SystemTime) -> u64 {
time.duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
}
pub fn plugins_dir(environment: &impl Environment) -> PathBuf {
environment.get_cache_dir().join("plugins")
}
pub fn wasm_artifact_path(hash: &str, environment: &impl Environment) -> PathBuf {
plugins_dir(environment).join(format!("{hash}.cwasm"))
}
pub fn process_dir_path(hash: &str, environment: &impl Environment) -> PathBuf {
plugins_dir(environment).join(hash)
}
fn meta_path(hash: &str, environment: &impl Environment) -> PathBuf {
plugins_dir(environment).join(format!("{hash}.json"))
}
#[cfg(test)]
mod test {
use super::*;
use crate::environment::TestEnvironment;
use pretty_assertions::assert_eq;
fn make_meta(signature: &str) -> PluginCacheMeta {
PluginCacheMeta {
source: "remote:https://example.com/test.wasm".to_string(),
signature: signature.to_string(),
plugin_kind: PluginKind::Wasm,
created_time: 123,
info: PluginInfo {
name: "test-plugin".to_string(),
version: "0.1.0".to_string(),
config_key: "test".to_string(),
help_url: "help".to_string(),
config_schema_url: "schema".to_string(),
update_url: None,
},
executable_sub_path: None,
local_stamps: None,
}
}
#[test]
fn should_roundtrip_meta() {
let environment = TestEnvironment::new();
environment.mk_dir_all(plugins_dir(&environment)).unwrap();
let meta = make_meta(¤t_signature(&environment));
write_meta("abc123", &meta, &environment).unwrap();
assert_eq!(read_meta("abc123", &environment), Some(meta));
}
#[test]
fn read_meta_is_none_for_missing_or_corrupt() {
let environment = TestEnvironment::new();
assert_eq!(read_meta("missing", &environment), None);
environment.mk_dir_all(plugins_dir(&environment)).unwrap();
environment.write_file(&plugins_dir(&environment).join("bad.json"), "{ not json").unwrap();
assert_eq!(read_meta("bad", &environment), None);
}
#[test]
fn remove_entry_deletes_meta_and_both_artifact_forms() {
let environment = TestEnvironment::new();
let dir = plugins_dir(&environment);
environment.mk_dir_all(&dir).unwrap();
write_meta("h", &make_meta("sig"), &environment).unwrap();
environment.write_file(&wasm_artifact_path("h", &environment), "compiled").unwrap();
environment.mk_dir_all(process_dir_path("h", &environment)).unwrap();
environment.write_file(&process_dir_path("h", &environment).join("exe"), "bin").unwrap();
remove_entry("h", &environment);
assert!(read_meta("h", &environment).is_none());
assert!(!environment.path_exists(&wasm_artifact_path("h", &environment)));
assert!(!environment.path_exists(&process_dir_path("h", &environment)));
}
}