use crate::LixError;
const PLUGIN_STORAGE_ROOT_PATH: &str = "/.lix/plugins";
const PLUGIN_STORAGE_ROOT_PREFIX: &str = "/.lix/plugins/";
pub const PLUGIN_ARCHIVE_FILE_EXTENSION: &str = ".lixplugin";
const PLUGIN_ARCHIVE_ID_NAMESPACE: uuid::Uuid =
uuid::Uuid::from_u128(0x6c69782d_706c_7567_696e_2d6172636869);
const PLUGIN_ARCHIVE_DELETE_ORIGIN_PREFIX: &str = "plugin_archive_delete:";
pub fn plugin_storage_archive_file_id(plugin_key: &str) -> String {
uuid::Uuid::new_v5(&PLUGIN_ARCHIVE_ID_NAMESPACE, plugin_key.as_bytes()).to_string()
}
pub fn plugin_storage_archive_path(plugin_key: &str) -> String {
format!("{PLUGIN_STORAGE_ROOT_PREFIX}{plugin_key}{PLUGIN_ARCHIVE_FILE_EXTENSION}")
}
pub fn plugin_key_from_archive_path(path: &str) -> Option<String> {
let file_name = path.strip_prefix(PLUGIN_STORAGE_ROOT_PREFIX)?;
let plugin_key = file_name.strip_suffix(PLUGIN_ARCHIVE_FILE_EXTENSION)?;
if !is_valid_plugin_key(plugin_key) {
return None;
}
Some(plugin_key.to_string())
}
pub fn plugin_archive_file_id_matches(file_id: &str, plugin_key: &str) -> bool {
is_valid_plugin_key(plugin_key) && file_id == plugin_storage_archive_file_id(plugin_key)
}
pub(crate) fn plugin_archive_delete_origin(plugin_key: &str) -> String {
format!("{PLUGIN_ARCHIVE_DELETE_ORIGIN_PREFIX}{plugin_key}")
}
pub(crate) fn plugin_key_from_archive_delete_origin(surface: &str) -> Option<&str> {
let plugin_key = surface.strip_prefix(PLUGIN_ARCHIVE_DELETE_ORIGIN_PREFIX)?;
is_valid_plugin_key(plugin_key).then_some(plugin_key)
}
fn is_valid_plugin_key(plugin_key: &str) -> bool {
if plugin_key.is_empty() || plugin_key.len() > 128 {
return false;
}
let mut bytes = plugin_key.bytes();
matches!(bytes.next(), Some(b'a'..=b'z'))
&& bytes.all(|byte| matches!(byte, b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-'))
}
pub(crate) fn reject_normal_plugin_storage_mutation(
path: &str,
operation: &str,
) -> Result<(), LixError> {
if !is_plugin_storage_path(path) {
return Ok(());
}
Err(LixError::new(
LixError::CODE_CONSTRAINT_VIOLATION,
format!("{operation} cannot modify reserved plugin storage path {path:?}"),
)
.with_hint("Write a valid plugin archive file to the plugin storage path to install it."))
}
pub(crate) fn is_plugin_storage_path(path: &str) -> bool {
path == PLUGIN_STORAGE_ROOT_PATH || path.starts_with(PLUGIN_STORAGE_ROOT_PREFIX)
}
#[cfg(test)]
mod tests {
use crate::LixError;
use super::{
plugin_archive_file_id_matches, plugin_key_from_archive_path,
plugin_storage_archive_file_id, plugin_storage_archive_path,
reject_normal_plugin_storage_mutation,
};
#[test]
fn computes_storage_archive_paths() {
assert_eq!(
plugin_storage_archive_path("plugin_json"),
"/.lix/plugins/plugin_json.lixplugin"
);
}
#[test]
fn extracts_plugin_key_from_storage_path() {
assert_eq!(
plugin_key_from_archive_path("/.lix/plugins/plugin_json.lixplugin"),
Some("plugin_json".to_string())
);
for path in [
"/.lix/plugins/plugin\\json.lixplugin",
"/.lix/plugins/nested/plugin.lixplugin",
"/.lix/plugins/PluginJson.lixplugin",
"/.lix/plugins/.lixplugin",
] {
assert_eq!(plugin_key_from_archive_path(path), None);
}
}
#[test]
fn archive_file_id_round_trips_only_canonical_plugin_keys() {
for key in ["plugin_json", "a", "plugin-2"] {
let file_id = plugin_storage_archive_file_id(key);
assert!(plugin_archive_file_id_matches(&file_id, key));
}
for file_id in [
"lix_plugin_archive::",
"lix_plugin_archive::PluginJson",
"lix_plugin_archive::plugin/nested",
"lix_plugin_archive::plugin_json::suffix",
"arbitrary-file-id",
] {
assert!(!plugin_archive_file_id_matches(file_id, "plugin_json"));
}
}
#[test]
fn rejects_normal_mutations_to_plugin_storage_paths() {
for path in [
"/.lix/plugins",
"/.lix/plugins/plugin_json.lixplugin",
"/.lix/plugins/nested/file.txt",
] {
let error = reject_normal_plugin_storage_mutation(path, "lix_file write")
.expect_err("plugin storage paths should be reserved");
assert_eq!(error.code, LixError::CODE_CONSTRAINT_VIOLATION);
assert!(error.message.contains("reserved plugin storage path"));
assert!(
error
.hint
.as_deref()
.is_some_and(|hint| hint.contains("plugin archive file"))
);
}
reject_normal_plugin_storage_mutation("/.lix/plugins-adjacent/file.txt", "lix_file write")
.expect("adjacent paths should remain writable");
}
}