use anyhow::Result;
use lowfat_core::pipeline::{apply_builtin, proc_normalize, Pipeline, StageType};
use lowfat_plugin::discovery::DiscoveredPlugin;
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(),
};
if let Err(e) = security::validate_plugin(manifest, &plugin.base_dir) {
anyhow::bail!("security check failed for '{}': {e}", manifest.plugin.name);
}
let filter = ProcessFilter {
info,
entry: entry_path,
base_dir: plugin.base_dir.clone(),
};
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 {
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(_) => {}
}
continue;
}
if stage.stage_type == StageType::Builtin {
if let Some(processed) = apply_builtin(&stage.name, &text, input_template.level, stage.param, stage.pattern.as_deref()) {
text = processed;
}
}
}
Ok(proc_normalize(&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\n"); }
#[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["));
}
}