rneter 0.4.3

SSH connection manager for network devices with intelligent state machine handling
Documentation
//! Device state machine handler for network devices.
//!
//! This module provides a sophisticated state machine implementation for managing
//! network device interactions through SSH. It handles prompt detection, automatic
//! state transitions, and intelligent command routing based on the current device state.

use std::collections::HashMap;

use once_cell::sync::Lazy;
use regex::{Regex, RegexSet};

mod builder;
mod config;
mod diagnostics;
mod execution;
mod runtime;
mod transitions;

pub use config::{
    DeviceCommandExecutionConfig, DeviceHandlerConfig, DeviceInputRule, DevicePromptRule,
    DevicePromptWithSysRule, DeviceShellFlavor, DeviceTransitionRule, input_rule, prompt_rule,
    prompt_with_sys_rule, transition_rule,
};
pub use diagnostics::StateMachineDiagnostics;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CommandExecutionStrategy {
    PromptDriven,
    ShellExitStatus {
        marker: String,
        shell_flavor: DeviceShellFlavor,
    },
}

pub struct DeviceHandler {
    /// Index of the current state in the `all_states` vector
    current_state_index: usize,

    /// All possible states the device can be in
    all_states: Vec<String>,

    /// Combined regex set for matching all state patterns
    all_regex: RegexSet,

    /// Maps regex match index to state index
    regex_index_map: HashMap<usize, usize>,

    /// Index range for prompt states in `all_states` (start, end)
    prompt_index: (usize, usize),

    /// Index range for system-specific prompts in `all_states` (start, end)
    sys_prompt_index: (usize, usize),

    /// Maps state to input requirements:
    /// - bool: whether the value is dynamic (from `dyn_param`)
    /// - String: the input value or key in `dyn_param`
    /// - bool: whether to record this input in the output
    input_map: HashMap<String, (bool, String, bool)>,

    /// State transition graph: (from_state, command, to_state, is_exit, needs_format)
    /// Used for pathfinding during active state transitions
    edges: Vec<(String, String, String, bool, bool)>,

    /// Regex patterns for errors that should be ignored
    ignore_errors: Option<RegexSet>,

    /// Dynamic parameters for input substitution (e.g., passwords, system names)
    pub dyn_param: HashMap<String, String>,

    /// Maps state index to (regex, capture_group_name) for extracting values from prompts
    catch_map: HashMap<usize, (Regex, String)>,

    /// Captured system name from the prompt (e.g., hostname)
    sys: Option<String>,

    /// Last prompt text matched by the state machine.
    current_prompt: Option<String>,

    /// Prompt regex patterns grouped by state (for diagnostics).
    prompt_patterns: Vec<(String, String)>,

    /// Strategy used to determine command success for this handler.
    command_execution: CommandExecutionStrategy,
}

type ExitPath = Option<(String, Vec<(String, String)>)>;

/// Predefined states that exist in every device handler.
static PRE_STATE: Lazy<Vec<String>> = Lazy::new(|| {
    vec![
        "Output".to_string(),
        "More".to_string(),
        "Error".to_string(),
    ]
});

/// Regex pattern for matching and removing control characters at the start of lines.
///
/// This pattern matches carriage returns and backspace characters that may appear
/// at the beginning of terminal output, which can interfere with proper line parsing.
pub static IGNORE_START_LINE: Lazy<Regex> =
    Lazy::new(
        || match Regex::new(r"^(\r+(\s+\r+)*)|(\u{8}+(\s+\u{8}+)*)") {
            Ok(re) => re,
            Err(err) => panic!("invalid IGNORE_START_LINE regex: {err}"),
        },
    );

/// Regex pattern for stripping OSC escape sequences such as xterm title updates.
pub static STRIP_OSC_ESCAPE: Lazy<Regex> =
    Lazy::new(|| match Regex::new(r"\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)") {
        Ok(re) => re,
        Err(err) => panic!("invalid STRIP_OSC_ESCAPE regex: {err}"),
    });

/// Regex pattern for stripping DCS escape sequences such as fish terminal probes.
pub static STRIP_DCS_ESCAPE: Lazy<Regex> = Lazy::new(|| match Regex::new(r"\x1BP(?s:.*?)\x1B\\") {
    Ok(re) => re,
    Err(err) => panic!("invalid STRIP_DCS_ESCAPE regex: {err}"),
});

/// Regex pattern for stripping CSI escape sequences such as `\x1b[?1034h`.
pub static STRIP_CSI_ESCAPE: Lazy<Regex> =
    Lazy::new(|| match Regex::new(r"\x1B\[[0-?]*[ -/]*[@-~]") {
        Ok(re) => re,
        Err(err) => panic!("invalid STRIP_CSI_ESCAPE regex: {err}"),
    });

/// Regex pattern for stripping simple escape sequences such as `\x1b=` and `\x1b>`.
pub static STRIP_SIMPLE_ESCAPE: Lazy<Regex> =
    Lazy::new(|| match Regex::new(r"\x1B(?:[@-Z\\-_]|[=>])") {
        Ok(re) => re,
        Err(err) => panic!("invalid STRIP_SIMPLE_ESCAPE regex: {err}"),
    });

#[cfg(test)]
fn build_test_handler() -> DeviceHandler {
    let mut dyn_param = HashMap::new();
    dyn_param.insert("EnablePassword".to_string(), "secret\n".to_string());

    DeviceHandler::new(DeviceHandlerConfig {
        prompt: vec![
            prompt_rule("Login", &[r"^dev>\s*$"]),
            prompt_rule("Enable", &[r"^dev#\s*$"]),
            prompt_rule("Config", &[r"^dev\(cfg\)#\s*$"]),
        ],
        write: vec![
            input_rule(
                "EnablePassword",
                true,
                "EnablePassword",
                true,
                &[r"^Password:\s*$"],
            ),
            input_rule("Confirm", false, "y", false, &[r"^\[y\/n\]\?\s*$"]),
        ],
        more_regex: vec![r"^--More--$".to_string()],
        error_regex: vec![r"^ERROR: .+$".to_string()],
        edges: vec![
            transition_rule("Login", "enable", "Enable", false, false),
            transition_rule("Enable", "configure terminal", "Config", false, false),
            transition_rule("Config", "exit", "Enable", true, false),
            transition_rule("Enable", "exit", "Login", true, false),
        ],
        ignore_errors: vec![r"^ERROR: benign$".to_string()],
        dyn_param,
        ..Default::default()
    })
    .expect("test handler config should be valid")
}