simulator-api 0.20.0

Wire-protocol types for the Solana simulator backtest WebSocket API
Documentation
//! `User-Agent` contract between our clients and the manager. Rendered by
//! `simulator-client` / `sim-cli`, parsed by the manager on the control-WS
//! upgrade. Both sides reference these consts so the two cannot drift.

pub const PRODUCT_SDK: &str = "nitro-sim-client";
pub const PRODUCT_CLI: &str = "nitro-sim-cli";
pub const MANAGED_MARKER: &str = "(managed)";

/// Tag value when no known product token was found. Never an empty string:
/// line protocol handles empty tag values inconsistently.
pub const UNKNOWN_VERSION: &str = "unknown";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientProduct {
    Sdk,
    Cli,
}

impl ClientProduct {
    const fn token(self) -> &'static str {
        match self {
            Self::Sdk => PRODUCT_SDK,
            Self::Cli => PRODUCT_CLI,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientSurface {
    Raw,
    Managed,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClientKind {
    Managed,
    Raw,
    Cli,
    Custom,
    /// Sent no User-Agent: either a pre-upgrade SDK or a bare custom client.
    Unknown,
}

impl ClientKind {
    /// Dashboard-visible: these values MUST stay byte-identical once shipped.
    pub const fn as_metric_value(self) -> &'static str {
        match self {
            Self::Managed => "managed",
            Self::Raw => "raw",
            Self::Cli => "cli",
            Self::Custom => "custom",
            Self::Unknown => "unknown",
        }
    }
}

/// `version` names the leading product's own release; `sdk_version` the client library's. They
/// differ for the CLI, whose binary embeds the SDK — each token then reports its own release.
pub fn render(
    product: ClientProduct,
    surface: ClientSurface,
    version: &str,
    sdk_version: &str,
) -> String {
    let mut user_agent = format!("{}/{version}", product.token());
    if product == ClientProduct::Cli {
        user_agent.push(' ');
        user_agent.push_str(PRODUCT_SDK);
        user_agent.push('/');
        user_agent.push_str(sdk_version);
    }
    if surface == ClientSurface::Managed {
        user_agent.push(' ');
        user_agent.push_str(MANAGED_MARKER);
    }
    user_agent
}

/// Classify by the **leading** product token, so the CLI's own token wins over
/// the SDK token it also carries. The version is returned only for a token we
/// recognize, which keeps the `client_version` metric tag bounded by our own
/// releases rather than by arbitrary strings.
pub fn classify(user_agent: Option<&str>) -> (ClientKind, &str) {
    let Some(user_agent) = user_agent.map(str::trim).filter(|value| !value.is_empty()) else {
        return (ClientKind::Unknown, UNKNOWN_VERSION);
    };
    let Some((token, version)) = user_agent
        .split_whitespace()
        .next()
        .and_then(|leading| leading.split_once('/'))
    else {
        return (ClientKind::Custom, UNKNOWN_VERSION);
    };
    let version = accepted_version(version);
    match token {
        PRODUCT_CLI => (ClientKind::Cli, version),
        PRODUCT_SDK if user_agent.contains(MANAGED_MARKER) => (ClientKind::Managed, version),
        PRODUCT_SDK => (ClientKind::Raw, version),
        _ => (ClientKind::Custom, UNKNOWN_VERSION),
    }
}

/// The header travels verbatim into the metrics tag, so a version is accepted only when it
/// looks like one of ours: `[A-Za-z0-9_.-]`, at most this long. Anything else reads as unknown
/// rather than reaching the metrics infrastructure.
const MAX_VERSION_LEN: usize = 32;

fn accepted_version(version: &str) -> &str {
    let valid = !version.is_empty()
        && version.len() <= MAX_VERSION_LEN
        && version
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'));
    if valid { version } else { UNKNOWN_VERSION }
}

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use super::*;

    #[rstest]
    #[case::raw_sdk(ClientProduct::Sdk, ClientSurface::Raw, ClientKind::Raw)]
    #[case::managed_sdk(ClientProduct::Sdk, ClientSurface::Managed, ClientKind::Managed)]
    #[case::cli_raw(ClientProduct::Cli, ClientSurface::Raw, ClientKind::Cli)]
    #[case::cli_managed(ClientProduct::Cli, ClientSurface::Managed, ClientKind::Cli)]
    fn rendered_identities_classify_back(
        #[case] product: ClientProduct,
        #[case] surface: ClientSurface,
        #[case] expected: ClientKind,
    ) {
        let user_agent = render(product, surface, "0.12.3", "0.12.3");
        assert_eq!(classify(Some(&user_agent)), (expected, "0.12.3"));
    }

    #[rstest]
    #[case::absent(None, ClientKind::Unknown)]
    #[case::empty(Some(""), ClientKind::Unknown)]
    #[case::python(Some("python-requests/2.31.0"), ClientKind::Custom)]
    #[case::garbage(Some("!!!"), ClientKind::Custom)]
    #[case::foreign_managed_marker(Some("my-bot/1.0 (managed)"), ClientKind::Custom)]
    fn foreign_and_absent_user_agents(
        #[case] user_agent: Option<&str>,
        #[case] expected: ClientKind,
    ) {
        assert_eq!(classify(user_agent), (expected, UNKNOWN_VERSION));
    }

    #[test]
    fn cli_token_wins_over_the_sdk_token_it_carries() {
        let user_agent = format!("{PRODUCT_CLI}/1.2.3 {PRODUCT_SDK}/0.12.3 {MANAGED_MARKER}");
        assert_eq!(classify(Some(&user_agent)), (ClientKind::Cli, "1.2.3"));
    }

    #[rstest]
    #[case::semver("1.2.3-rc.1_hotfix", "1.2.3-rc.1_hotfix")]
    #[case::injection("1.2.3\",attack=\"x", UNKNOWN_VERSION)]
    #[case::overlong("1.0.0-padpadpadpadpadpadpadpadpadpad", UNKNOWN_VERSION)]
    #[case::empty_version("", UNKNOWN_VERSION)]
    fn version_tag_is_bounded_before_reaching_metrics(
        #[case] version: &str,
        #[case] expected: &str,
    ) {
        let user_agent = format!("{PRODUCT_CLI}/{version}");
        assert_eq!(classify(Some(&user_agent)), (ClientKind::Cli, expected));
    }
}