use anyhow::Result;
use lowfat_core::pipeline::{apply_builtin, Pipeline, StageType};
use lowfat_plugin::discovery::DiscoveredPlugin;
use lowfat_plugin::manifest::RuntimeType;
use lowfat_plugin::plugin::{FilterInput, FilterPlugin, PluginInfo};
use lowfat_plugin::security;
use std::collections::HashMap;
use crate::process::ProcessFilter;
pub struct HybridRunner;
impl HybridRunner {
pub fn load(plugin: &DiscoveredPlugin) -> Result<Box<dyn FilterPlugin>> {
let manifest = &plugin.manifest;
let entry_path = plugin.base_dir.join(&manifest.runtime.entry);
let info = PluginInfo {
name: manifest.plugin.name.clone(),
version: manifest
.plugin
.version
.clone()
.unwrap_or_else(|| "0.0.0".to_string()),
commands: manifest.plugin.commands.clone(),
subcommands: manifest
.plugin
.subcommands
.clone()
.unwrap_or_default(),
};
let input_format = manifest
.input
.as_ref()
.and_then(|io| io.format.clone())
.unwrap_or_else(|| "raw".to_string());
let result_format = manifest
.result
.as_ref()
.and_then(|io| io.format.clone())
.unwrap_or_else(|| "raw".to_string());
if let Err(e) = security::validate_plugin(manifest, &plugin.base_dir) {
anyhow::bail!("security check failed for '{}': {e}", manifest.plugin.name);
}
match manifest.runtime.runtime_type {
RuntimeType::Wasm => {
anyhow::bail!(
"WASM plugins not yet supported. Plugin '{}' requires WASM runtime.",
manifest.plugin.name
)
}
_ => {
let filter = ProcessFilter {
info,
runtime_type: manifest.runtime.runtime_type,
entry: entry_path,
base_dir: plugin.base_dir.clone(),
custom_command: manifest.runtime.command.clone(),
input_format,
result_format,
};
Ok(Box::new(filter))
}
}
}
}
pub fn execute_pipeline(
pipeline: &Pipeline,
raw: &str,
input_template: &FilterInput,
plugin_map: &HashMap<String, Box<dyn FilterPlugin>>,
) -> Result<String> {
let mut text = raw.to_string();
for stage in &pipeline.stages {
match stage.stage_type {
StageType::Builtin => {
if let Some(processed) = apply_builtin(&stage.name, &text, input_template.level, stage.param) {
text = processed;
}
}
StageType::Plugin => {
if let Some(filter) = plugin_map.get(&stage.name) {
let mut stage_input = input_template.clone();
stage_input.raw = text.clone();
match filter.filter(&stage_input) {
Ok(out) if !out.passthrough => {
text = out.text;
}
Ok(_) => {} Err(_) => {} }
}
}
}
}
Ok(text)
}
pub fn exec_command(cmd: &str, args: &[String]) -> Result<(String, i32)> {
let output = std::process::Command::new(cmd)
.args(args)
.output()?;
let exit_code = output.status.code().unwrap_or(1);
let mut combined = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr);
if !stderr.is_empty() {
if !combined.is_empty() {
combined.push('\n');
}
combined.push_str(&stderr);
}
Ok((combined, exit_code))
}
#[cfg(test)]
mod tests {
use super::*;
use lowfat_core::level::Level;
use lowfat_core::pipeline::Pipeline;
fn make_input(raw: &str) -> FilterInput {
FilterInput {
raw: raw.to_string(),
command: "test".to_string(),
subcommand: String::new(),
args: vec![],
level: Level::Full,
head_limit: 40,
exit_code: 0,
}
}
#[test]
fn execute_builtin_only_pipeline() {
let pipeline = Pipeline::parse("strip-ansi, dedup-blank");
let raw = "\x1b[31mERROR\x1b[0m\n\n\n\nline2";
let input = make_input(raw);
let result = execute_pipeline(&pipeline, raw, &input, &HashMap::new()).unwrap();
assert_eq!(result, "ERROR\n\nline2\n");
}
#[test]
fn execute_passthrough_pipeline() {
let pipeline = Pipeline::parse("passthrough");
let raw = "hello world";
let input = make_input(raw);
let result = execute_pipeline(&pipeline, raw, &input, &HashMap::new()).unwrap();
assert_eq!(result, "hello world");
}
#[test]
fn execute_truncate_pipeline() {
let pipeline = Pipeline::parse("head");
let raw = (0..100).map(|i| format!("line{i}")).collect::<Vec<_>>().join("\n");
let input = make_input(&raw);
let result = execute_pipeline(&pipeline, &raw, &input, &HashMap::new()).unwrap();
let line_count = result.lines().count();
assert!(line_count <= 41); }
#[test]
fn execute_chain_strip_then_truncate() {
let pipeline = Pipeline::parse("strip-ansi, head");
let mut raw = String::new();
for i in 0..100 {
raw.push_str(&format!("\x1b[32mline{i}\x1b[0m\n"));
}
let input = make_input(&raw);
let result = execute_pipeline(&pipeline, &raw, &input, &HashMap::new()).unwrap();
assert!(!result.contains("\x1b["));
assert!(result.lines().count() <= 41);
}
#[test]
fn missing_plugin_skipped() {
let pipeline = Pipeline::parse("strip-ansi, nonexistent-plugin, head");
let raw = "\x1b[31mhello\x1b[0m\nworld";
let input = make_input(raw);
let result = execute_pipeline(&pipeline, raw, &input, &HashMap::new()).unwrap();
assert!(result.contains("hello"));
assert!(!result.contains("\x1b["));
}
}