mihomo-rs 2.1.0

A Rust SDK and CLI tool for mihomo proxy management with service lifecycle management, configuration handling, and real-time monitoring
Documentation
use crate::core::ErrorCode;
use crate::MihomoError;

struct ErrorHintRule {
    code: ErrorCode,
    hint: &'static str,
}

const ERROR_HINT_RULES: &[ErrorHintRule] = &[
    ErrorHintRule {
        code: ErrorCode::InvalidExternalController,
        hint: "expected formats: 127.0.0.1:9090 | :9090 | http://host:9090 | https://host:9090 | /path/to/mihomo.sock | unix:///path/to/mihomo.sock",
    },
    ErrorHintRule {
        code: ErrorCode::InvalidProfileName,
        hint: "profile can only include letters, numbers, '.', '_' and '-'",
    },
    ErrorHintRule {
        code: ErrorCode::InvalidVersion,
        hint: "version can only include letters, numbers, '.', '_', '-' and '+'",
    },
];

fn hint_for_error_code(code: ErrorCode) -> Option<&'static str> {
    ERROR_HINT_RULES
        .iter()
        .find(|rule| rule.code == code)
        .map(|rule| rule.hint)
}

pub fn format_cli_error(err: &anyhow::Error) -> String {
    if let Some(MihomoError::Config(msg) | MihomoError::Version(msg)) =
        err.downcast_ref::<MihomoError>()
    {
        let plain = msg.message.clone();
        let hint = msg.code.and_then(hint_for_error_code);
        if let Some(hint) = hint {
            return format!("Error: {}\nHint: {}", plain, hint);
        }
        return format!("Error: {}", plain);
    }
    format!("Error: {}", err)
}

#[cfg(test)]
mod tests {
    use super::format_cli_error;
    use crate::core::ErrorCode;
    use crate::MihomoError;

    #[test]
    fn format_cli_error_adds_hint_for_invalid_external_controller() {
        let err = anyhow::Error::new(MihomoError::config_with_code(
            ErrorCode::InvalidExternalController,
            "Invalid external-controller value '://invalid'",
        ));
        let rendered = format_cli_error(&err);
        assert!(rendered.contains("Invalid external-controller value '://invalid'"));
        assert!(rendered.contains("Hint: expected formats:"));
    }

    #[test]
    fn format_cli_error_adds_hint_for_invalid_profile_name() {
        let err = anyhow::Error::new(MihomoError::config_with_code(
            ErrorCode::InvalidProfileName,
            "Invalid profile name '../evil'",
        ));
        let rendered = format_cli_error(&err);
        assert!(rendered.contains("Hint: profile can only include"));
    }

    #[test]
    fn format_cli_error_adds_hint_for_invalid_version() {
        let err = anyhow::Error::new(MihomoError::version_with_code(
            ErrorCode::InvalidVersion,
            "Invalid version '../v1'",
        ));
        let rendered = format_cli_error(&err);
        assert!(rendered.contains("Invalid version '../v1'"));
        assert!(rendered.contains("Hint: version can only include"));
    }

    #[test]
    fn format_cli_error_falls_back_for_other_errors() {
        let err = anyhow::Error::new(MihomoError::NotFound("Profile 'x' not found".to_string()));
        let rendered = format_cli_error(&err);
        assert_eq!(rendered, "Error: Not found: Profile 'x' not found");
    }
}