player_plugin/
protocol.rs1use 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;
19pub const MAX_PIPELINE_EVENT_INPUT_BYTES: usize = 256 * 1024;
21pub 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}