use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HookType {
PreWrite,
PostWrite,
PreRead,
PostRead,
OnCreate,
OnDelete,
Compress,
Decompress,
Validate,
Transform,
}
impl HookType {
pub fn as_str(&self) -> &'static str {
match self {
HookType::PreWrite => "PreWrite",
HookType::PostWrite => "PostWrite",
HookType::PreRead => "PreRead",
HookType::PostRead => "PostRead",
HookType::OnCreate => "OnCreate",
HookType::OnDelete => "OnDelete",
HookType::Compress => "Compress",
HookType::Decompress => "Decompress",
HookType::Validate => "Validate",
HookType::Transform => "Transform",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"PreWrite" => Some(HookType::PreWrite),
"PostWrite" => Some(HookType::PostWrite),
"PreRead" => Some(HookType::PreRead),
"PostRead" => Some(HookType::PostRead),
"OnCreate" => Some(HookType::OnCreate),
"OnDelete" => Some(HookType::OnDelete),
"Compress" => Some(HookType::Compress),
"Decompress" => Some(HookType::Decompress),
"Validate" => Some(HookType::Validate),
"Transform" => Some(HookType::Transform),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Permission {
ReadContent,
WriteContent,
ReadMetadata,
WriteMetadata,
Network,
PathAccess(String),
MemoryLimit(usize),
TimeLimit(u64),
}
impl Permission {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"ReadContent" => return Some(Permission::ReadContent),
"WriteContent" => return Some(Permission::WriteContent),
"ReadMetadata" => return Some(Permission::ReadMetadata),
"WriteMetadata" => return Some(Permission::WriteMetadata),
"Network" => return Some(Permission::Network),
_ => {}
}
if let Some(path) = s.strip_prefix("PathAccess:") {
return Some(Permission::PathAccess(path.to_string()));
}
if let Some(limit) = s.strip_prefix("MemoryLimit:") {
if let Ok(n) = limit.parse() {
return Some(Permission::MemoryLimit(n));
}
}
if let Some(limit) = s.strip_prefix("TimeLimit:") {
if let Ok(n) = limit.parse() {
return Some(Permission::TimeLimit(n));
}
}
None
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PluginManifest {
pub name: String,
pub version: String,
pub author: String,
pub description: String,
pub hooks: Vec<HookType>,
pub permissions: Vec<Permission>,
}
impl Default for PluginManifest {
fn default() -> Self {
Self {
name: String::new(),
version: "0.0.0".into(),
author: "Unknown".into(),
description: String::new(),
hooks: Vec::new(),
permissions: Vec::new(),
}
}
}
impl PluginManifest {
pub fn new(name: &str, version: &str) -> Self {
Self {
name: name.into(),
version: version.into(),
..Default::default()
}
}
pub fn from_json(json: &str) -> Result<Self, ManifestError> {
let mut manifest = PluginManifest::default();
let json = json.trim();
if !json.starts_with('{') || !json.ends_with('}') {
return Err(ManifestError::InvalidFormat);
}
if let Some(name) = extract_string_field(json, "name") {
manifest.name = name;
} else {
return Err(ManifestError::MissingField("name"));
}
if let Some(version) = extract_string_field(json, "version") {
manifest.version = version;
}
if let Some(author) = extract_string_field(json, "author") {
manifest.author = author;
}
if let Some(description) = extract_string_field(json, "description") {
manifest.description = description;
}
if let Some(hooks_str) = extract_array_field(json, "hooks") {
for hook_str in parse_string_array(&hooks_str) {
if let Some(hook) = HookType::from_str(&hook_str) {
manifest.hooks.push(hook);
}
}
}
if let Some(perms_str) = extract_array_field(json, "permissions") {
for perm_str in parse_string_array(&perms_str) {
if let Some(perm) = Permission::from_str(&perm_str) {
manifest.permissions.push(perm);
}
}
}
Ok(manifest)
}
pub fn has_permission(&self, perm: &Permission) -> bool {
self.permissions.iter().any(|p| {
match (p, perm) {
(Permission::PathAccess(pattern), Permission::PathAccess(path)) => {
if pattern.ends_with('*') {
let prefix = &pattern[..pattern.len() - 1];
path.starts_with(prefix)
} else {
pattern == path
}
}
_ => p == perm,
}
})
}
pub fn implements_hook(&self, hook: HookType) -> bool {
self.hooks.contains(&hook)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManifestError {
InvalidFormat,
MissingField(&'static str),
InvalidField(&'static str),
}
#[derive(Debug, Clone, Default)]
pub struct PluginContext {
pub path: String,
pub size: u64,
pub dataset: String,
pub operation: String,
pub metadata: BTreeMap<String, String>,
}
impl PluginContext {
pub fn new(path: &str, dataset: &str, operation: &str) -> Self {
Self {
path: path.into(),
dataset: dataset.into(),
operation: operation.into(),
..Default::default()
}
}
pub fn with_size(mut self, size: u64) -> Self {
self.size = size;
self
}
pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn to_json(&self) -> String {
let mut json = String::from("{");
json.push_str(&alloc::format!("\"path\":\"{}\",", escape_json(&self.path)));
json.push_str(&alloc::format!("\"size\":{},", self.size));
json.push_str(&alloc::format!(
"\"dataset\":\"{}\",",
escape_json(&self.dataset)
));
json.push_str(&alloc::format!(
"\"operation\":\"{}\",",
escape_json(&self.operation)
));
json.push_str("\"metadata\":{");
let mut first = true;
for (k, v) in &self.metadata {
if !first {
json.push(',');
}
json.push_str(&alloc::format!(
"\"{}\":\"{}\"",
escape_json(k),
escape_json(v)
));
first = false;
}
json.push_str("}}");
json
}
}
#[derive(Debug, Clone)]
pub struct PluginLimits {
pub max_memory: usize,
pub max_execution_ms: u64,
pub max_output_size: usize,
pub max_stack_depth: usize,
}
impl Default for PluginLimits {
fn default() -> Self {
Self {
max_memory: 16 * 1024 * 1024, max_execution_ms: 1000, max_output_size: 64 * 1024 * 1024, max_stack_depth: 1024,
}
}
}
#[derive(Debug, Clone)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub author: String,
pub description: String,
pub hooks: Vec<HookType>,
pub enabled: bool,
pub invocation_count: u64,
pub total_execution_us: u64,
}
impl From<&PluginManifest> for PluginInfo {
fn from(manifest: &PluginManifest) -> Self {
Self {
name: manifest.name.clone(),
version: manifest.version.clone(),
author: manifest.author.clone(),
description: manifest.description.clone(),
hooks: manifest.hooks.clone(),
enabled: true,
invocation_count: 0,
total_execution_us: 0,
}
}
}
fn extract_string_field(json: &str, field: &str) -> Option<String> {
let pattern = alloc::format!("\"{}\":", field);
let start = json.find(&pattern)?;
let rest = &json[start + pattern.len()..];
let rest = rest.trim_start();
if !rest.starts_with('"') {
return None;
}
let rest = &rest[1..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
fn extract_array_field(json: &str, field: &str) -> Option<String> {
let pattern = alloc::format!("\"{}\":", field);
let start = json.find(&pattern)?;
let rest = &json[start + pattern.len()..];
let rest = rest.trim_start();
if !rest.starts_with('[') {
return None;
}
let mut depth = 0;
let mut end = 0;
for (i, c) in rest.chars().enumerate() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
end = i;
break;
}
}
_ => {}
}
}
Some(rest[1..end].to_string())
}
fn parse_string_array(s: &str) -> Vec<String> {
let mut result = Vec::new();
let mut in_string = false;
let mut current = String::new();
for c in s.chars() {
match c {
'"' => {
if in_string {
result.push(current.clone());
current.clear();
}
in_string = !in_string;
}
_ if in_string => {
current.push(c);
}
_ => {}
}
}
result
}
fn escape_json(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_hook_type_round_trip() {
let hooks = [
HookType::PreWrite,
HookType::PostWrite,
HookType::PreRead,
HookType::PostRead,
HookType::OnCreate,
HookType::OnDelete,
HookType::Compress,
HookType::Decompress,
HookType::Validate,
HookType::Transform,
];
for hook in hooks {
let s = hook.as_str();
let parsed = HookType::from_str(s).unwrap();
assert_eq!(hook, parsed);
}
}
#[test]
fn test_permission_parsing() {
assert_eq!(
Permission::from_str("ReadContent"),
Some(Permission::ReadContent)
);
assert_eq!(
Permission::from_str("WriteContent"),
Some(Permission::WriteContent)
);
assert_eq!(
Permission::from_str("PathAccess:/data/*"),
Some(Permission::PathAccess("/data/*".into()))
);
assert_eq!(
Permission::from_str("MemoryLimit:1048576"),
Some(Permission::MemoryLimit(1048576))
);
assert_eq!(
Permission::from_str("TimeLimit:5000"),
Some(Permission::TimeLimit(5000))
);
}
#[test]
fn test_manifest_from_json() {
let json = r#"{
"name": "test-plugin",
"version": "1.0.0",
"author": "Test Author",
"description": "A test plugin",
"hooks": ["PreWrite", "PostRead"],
"permissions": ["ReadContent", "WriteContent"]
}"#;
let manifest = PluginManifest::from_json(json).unwrap();
assert_eq!(manifest.name, "test-plugin");
assert_eq!(manifest.version, "1.0.0");
assert_eq!(manifest.author, "Test Author");
assert_eq!(manifest.hooks.len(), 2);
assert!(manifest.hooks.contains(&HookType::PreWrite));
assert!(manifest.hooks.contains(&HookType::PostRead));
assert_eq!(manifest.permissions.len(), 2);
}
#[test]
fn test_manifest_missing_name() {
let json = r#"{"version": "1.0.0"}"#;
let result = PluginManifest::from_json(json);
assert_eq!(result, Err(ManifestError::MissingField("name")));
}
#[test]
fn test_manifest_has_permission() {
let mut manifest = PluginManifest::new("test", "1.0");
manifest.permissions.push(Permission::ReadContent);
manifest
.permissions
.push(Permission::PathAccess("/data/*".into()));
assert!(manifest.has_permission(&Permission::ReadContent));
assert!(!manifest.has_permission(&Permission::WriteContent));
assert!(manifest.has_permission(&Permission::PathAccess("/data/file.txt".into())));
assert!(!manifest.has_permission(&Permission::PathAccess("/other/file.txt".into())));
}
#[test]
fn test_plugin_context_json() {
let ctx = PluginContext::new("/path/to/file", "dataset1", "write")
.with_size(1024)
.with_metadata("key", "value");
let json = ctx.to_json();
assert!(json.contains("\"path\":\"/path/to/file\""));
assert!(json.contains("\"size\":1024"));
assert!(json.contains("\"dataset\":\"dataset1\""));
assert!(json.contains("\"operation\":\"write\""));
assert!(json.contains("\"key\":\"value\""));
}
#[test]
fn test_plugin_limits_default() {
let limits = PluginLimits::default();
assert_eq!(limits.max_memory, 16 * 1024 * 1024);
assert_eq!(limits.max_execution_ms, 1000);
assert_eq!(limits.max_output_size, 64 * 1024 * 1024);
}
#[test]
fn test_escape_json() {
assert_eq!(escape_json("hello"), "hello");
assert_eq!(escape_json("hello\"world"), "hello\\\"world");
assert_eq!(escape_json("line1\nline2"), "line1\\nline2");
assert_eq!(escape_json("path\\to\\file"), "path\\\\to\\\\file");
}
}