Skip to main content

player_plugin/
protocol.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6pub const MAX_PLUGIN_ATTRIBUTES: usize = 32;
7pub const MAX_PLUGIN_ATTRIBUTE_KEY_BYTES: usize = 64;
8pub const MAX_PLUGIN_ATTRIBUTE_VALUE_BYTES: usize = 256;
9pub const MAX_PLUGIN_DIAGNOSTIC_MESSAGE_BYTES: usize = 256;
10pub const MAX_PLUGIN_ERROR_MESSAGE_BYTES: usize = 256;
11pub const MAX_PLUGIN_MEASUREMENTS: usize = 128;
12pub const MAX_PLUGIN_DIAGNOSTICS: usize = 64;
13pub const MAX_PLUGIN_EVENT_ID_BYTES: usize = 128;
14pub const MAX_PLUGIN_EVENT_NAME_BYTES: usize = 64;
15pub const MAX_PLUGIN_PLATFORM_BYTES: usize = 64;
16pub const MAX_PLUGIN_PROTOCOL_BYTES: usize = 64;
17pub const MAX_PLUGIN_THREAD_BYTES: usize = 128;
18pub const MAX_PLUGIN_RESOURCE_IDENTITY_BYTES: usize = 256;
19/// Maximum encoded size of one pipeline event at a plugin transport boundary.
20pub const MAX_PIPELINE_EVENT_INPUT_BYTES: usize = 256 * 1024;
21/// Maximum byte length of one source-normalizer packet payload.
22pub const MAX_SOURCE_NORMALIZER_PACKET_BYTES: usize =
23    player_plugin_abi::VESPER_MAX_PACKET_BYTES as usize;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub enum PluginDiagnosticSeverity {
28    Info,
29    Warning,
30    Error,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct PluginDiagnostic {
36    pub code: String,
37    pub severity: PluginDiagnosticSeverity,
38    pub message: String,
39    #[serde(default)]
40    pub attributes: BTreeMap<String, String>,
41}
42
43impl PluginDiagnostic {
44    pub fn validate(&self) -> Result<(), PluginProtocolViolation> {
45        validate_text(
46            "diagnostic.code",
47            &self.code,
48            MAX_PLUGIN_ATTRIBUTE_KEY_BYTES,
49        )?;
50        validate_text(
51            "diagnostic.message",
52            &self.message,
53            MAX_PLUGIN_DIAGNOSTIC_MESSAGE_BYTES,
54        )?;
55        validate_attributes(&self.attributes)
56    }
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct PluginMeasurement {
62    pub name: String,
63    pub value: f64,
64    pub unit: String,
65    #[serde(default)]
66    pub attributes: BTreeMap<String, String>,
67}
68
69impl PluginMeasurement {
70    pub fn validate(&self) -> Result<(), PluginProtocolViolation> {
71        validate_text(
72            "measurement.name",
73            &self.name,
74            MAX_PLUGIN_ATTRIBUTE_KEY_BYTES,
75        )?;
76        validate_text(
77            "measurement.unit",
78            &self.unit,
79            MAX_PLUGIN_ATTRIBUTE_KEY_BYTES,
80        )?;
81        if !self.value.is_finite() {
82            return Err(PluginProtocolViolation::NonFiniteMeasurement {
83                name: self.name.clone(),
84            });
85        }
86        validate_attributes(&self.attributes)
87    }
88}
89
90#[derive(Debug, Error, Clone, PartialEq, Eq)]
91pub enum PluginProtocolViolation {
92    #[error("{field} must not be empty")]
93    Empty { field: &'static str },
94    #[error("{field} exceeds {limit} bytes")]
95    TooLong { field: &'static str, limit: usize },
96    #[error("plugin attributes exceed the {limit}-entry protocol limit")]
97    TooManyAttributes { limit: usize },
98    #[error("measurement `{name}` is not finite")]
99    NonFiniteMeasurement { name: String },
100}
101
102pub(crate) fn validate_attributes(
103    attributes: &BTreeMap<String, String>,
104) -> Result<(), PluginProtocolViolation> {
105    if attributes.len() > MAX_PLUGIN_ATTRIBUTES {
106        return Err(PluginProtocolViolation::TooManyAttributes {
107            limit: MAX_PLUGIN_ATTRIBUTES,
108        });
109    }
110    for (key, value) in attributes {
111        validate_text("attribute.key", key, MAX_PLUGIN_ATTRIBUTE_KEY_BYTES)?;
112        validate_text("attribute.value", value, MAX_PLUGIN_ATTRIBUTE_VALUE_BYTES)?;
113    }
114    Ok(())
115}
116
117pub(crate) fn validate_text(
118    field: &'static str,
119    value: &str,
120    limit: usize,
121) -> Result<(), PluginProtocolViolation> {
122    if value.is_empty() {
123        return Err(PluginProtocolViolation::Empty { field });
124    }
125    if value.len() > limit {
126        return Err(PluginProtocolViolation::TooLong { field, limit });
127    }
128    Ok(())
129}
130
131pub(crate) fn validate_optional_text(
132    field: &'static str,
133    value: Option<&str>,
134    limit: usize,
135) -> Result<(), PluginProtocolViolation> {
136    if let Some(value) = value {
137        validate_text(field, value, limit)?;
138    }
139    Ok(())
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn measurements_reject_non_finite_values() {
148        let measurement = PluginMeasurement {
149            name: "startup".to_owned(),
150            value: f64::NAN,
151            unit: "ms".to_owned(),
152            attributes: BTreeMap::new(),
153        };
154        assert!(matches!(
155            measurement.validate(),
156            Err(PluginProtocolViolation::NonFiniteMeasurement { .. })
157        ));
158    }
159
160    #[test]
161    fn diagnostics_enforce_named_protocol_limits() {
162        let diagnostic = PluginDiagnostic {
163            code: "event.accepted".to_owned(),
164            severity: PluginDiagnosticSeverity::Info,
165            message: "accepted".to_owned(),
166            attributes: (0..=MAX_PLUGIN_ATTRIBUTES)
167                .map(|index| (format!("key-{index}"), "value".to_owned()))
168                .collect(),
169        };
170        assert_eq!(
171            diagnostic.validate(),
172            Err(PluginProtocolViolation::TooManyAttributes {
173                limit: MAX_PLUGIN_ATTRIBUTES
174            })
175        );
176    }
177}