Skip to main content

krabka_raft/server/api_versions/
client_software.rs

1//! The KIP-511 `client_software_name` and `client_software_version` check that
2//! `ApiVersions` v3 and above applies to a handshake.
3//!
4//! The broker listener and both controller-listener paths (before and after
5//! SASL authentication) run the same check. So it lives here, and the broker
6//! re-exports it.
7
8/// KIP-511 client-information validity check. Matches the JVM
9/// `ApiVersionsRequest.isValid` regex
10/// `[a-zA-Z0-9](?:[a-zA-Z0-9\-.]*[a-zA-Z0-9])?`:
11///
12/// - non-empty
13/// - first and last chars are `[a-zA-Z0-9]`
14/// - interior chars are `[a-zA-Z0-9\-.]`
15///
16/// A single alphanumeric char is valid, because the optional middle group lets
17/// the first and last char coincide.
18///
19/// The check is a byte scan instead of a `regex` dependency. Every Kafka-client
20/// name in use stays within ASCII, so full UTF-8 char-class semantics are not
21/// needed.
22#[must_use]
23pub fn is_valid_client_info(s: &str) -> bool {
24    let is_alnum = |b: u8| b.is_ascii_alphanumeric();
25    let is_interior = |b: u8| b.is_ascii_alphanumeric() || b == b'-' || b == b'.';
26    match s.as_bytes() {
27        [] => false,
28        &[only] => is_alnum(only),
29        &[first, ref interior @ .., last] => {
30            is_alnum(first) && is_alnum(last) && interior.iter().all(|&b| is_interior(b))
31        }
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use {super::*, assert2::assert};
38
39    // ── KIP-511 client-info validation ─────────────────────────────────────
40
41    #[yare::parameterized(
42        apache_java = { "apache-kafka-java" },
43        krabka_client = { "krabka-client-core" },
44        librdkafka = { "librdkafka" },
45        kafka_python = { "kafka-python" },
46        node_rdkafka = { "node-rdkafka" },
47        sarama = { "Sarama" },
48        version = { "3.6.2" },
49        zero_version = { "0.0.0" },
50        snapshot_version = { "1.0.0-SNAPSHOT" },
51        single_alnum = { "a" },
52        four_part_version = { "1.2.3.4" },
53    )]
54    fn valid_client_info_accepts_typical_names(s: &str) {
55        assert2::assert!(is_valid_client_info(s), "{s:?} should be valid");
56    }
57
58    #[test]
59    fn valid_client_info_rejects_empty() {
60        assert!(!is_valid_client_info(""));
61    }
62
63    #[yare::parameterized(
64        leading_hyphen = { "-leading" },
65        trailing_hyphen = { "trailing-" },
66        leading_dot = { ".dotstart" },
67        trailing_dot = { "dotend." },
68        only_hyphens = { "-only-" },
69    )]
70    fn valid_client_info_rejects_leading_or_trailing_special(s: &str) {
71        assert2::assert!(!is_valid_client_info(s), "{s:?} should be rejected");
72    }
73
74    #[yare::parameterized(
75        space = { "has space" },
76        slash = { "has/slash" },
77        backslash = { "has\\backslash" },
78        semicolon = { "has;semi" },
79        at = { "has@at" },
80        parentheses = { "has(paren)" },
81        quote = { "has\"quote" },
82        non_ascii = { "café" },
83    )]
84    fn valid_client_info_rejects_disallowed_interior_chars(s: &str) {
85        // KIP-511's alphanumeric class is ASCII-only.
86        assert2::assert!(!is_valid_client_info(s), "{s:?} should be rejected");
87    }
88}