Skip to main content

bacnet_server/server/
rpm_budget.rs

1//! Per-service RPM limits, independent of concurrent handler admission.
2use super::*;
3
4/// Local ReadPropertyMultiple planning and encoded service-ACK limits.
5///
6/// These do not bound request decoding, object metadata, an individual property
7/// read/value encoding, allocator capacity, or process memory. Byte refusal does
8/// not roll back reads already performed.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct ReadPropertyMultipleBudget {
11    /// Maximum aggregate expanded result occurrences (default 256).
12    ///
13    /// ReadProperty, ReadRange and `read_local` charge a Group's Present_Value
14    /// to this limit as well. The shared endpoint (`bacnet-endpoint`) has no
15    /// `ServerConfig`: its ReadProperty uses the session's `read_work_limit`,
16    /// which has the same default and the same abort.
17    pub max_result_elements: usize,
18    /// Maximum encoded service parameters, excluding APDU/NPDU (default 16384).
19    pub max_service_ack_bytes: usize,
20}
21
22impl Default for ReadPropertyMultipleBudget {
23    fn default() -> Self {
24        Self {
25            max_result_elements: 256,
26            max_service_ack_bytes: 16384,
27        }
28    }
29}
30
31impl ReadPropertyMultipleBudget {
32    /// Reject zero; no semaphore-derived or allocation-capacity upper limit.
33    pub fn validate(&self) -> Result<(), Error> {
34        for (name, value) in [
35            ("rpm_max_result_elements", self.max_result_elements),
36            ("rpm_max_service_ack_bytes", self.max_service_ack_bytes),
37        ] {
38            if value == 0 {
39                return Err(Error::Encoding(format!("{name} must be positive")));
40            }
41        }
42        Ok(())
43    }
44}
45
46impl<T: TransportPort + 'static> ServerBuilder<T> {
47    /// Set positive per-service RPM limits, validated before transport startup.
48    pub fn read_property_multiple_budget(mut self, budget: ReadPropertyMultipleBudget) -> Self {
49        self.config.read_property_multiple_budget = budget;
50        self
51    }
52}
53
54impl BipServerBuilder {
55    /// Set positive per-service RPM limits, validated before transport startup.
56    pub fn read_property_multiple_budget(mut self, budget: ReadPropertyMultipleBudget) -> Self {
57        self.config.read_property_multiple_budget = budget;
58        self
59    }
60}
61
62#[cfg(feature = "sc-tls")]
63impl ScServerBuilder {
64    /// Set positive per-service RPM limits, validated before SC dialing.
65    pub fn read_property_multiple_budget(mut self, budget: ReadPropertyMultipleBudget) -> Self {
66        self.config.read_property_multiple_budget = budget;
67        self
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::server::test_transport::TestTransport;
75
76    #[tokio::test]
77    async fn rpm_defaults_builders_and_validation_before_start() {
78        let defaults = ReadPropertyMultipleBudget::default();
79        assert_eq!(
80            (defaults.max_result_elements, defaults.max_service_ack_bytes),
81            (256, 16384)
82        );
83        assert_eq!(
84            ServerConfig::default().read_property_multiple_budget,
85            defaults
86        );
87        assert!(format!("{:?}", ServerConfig::default()).contains("read_property_multiple_budget"));
88        ReadPropertyMultipleBudget {
89            max_result_elements: usize::MAX,
90            max_service_ack_bytes: usize::MAX,
91        }
92        .validate()
93        .unwrap();
94        for budget in [
95            ReadPropertyMultipleBudget {
96                max_result_elements: 0,
97                ..defaults
98            },
99            ReadPropertyMultipleBudget {
100                max_service_ack_bytes: 0,
101                ..defaults
102            },
103        ] {
104            let direct = BACnetServer::start(
105                ServerConfig {
106                    read_property_multiple_budget: budget,
107                    ..Default::default()
108                },
109                ObjectDatabase::new(),
110                TestTransport::never_start(),
111            )
112            .await;
113            let generic = BACnetServer::generic_builder()
114                .transport(TestTransport::never_start())
115                .read_property_multiple_budget(budget)
116                .build()
117                .await;
118            let bip = BACnetServer::bip_builder()
119                .port(0)
120                .read_property_multiple_budget(budget)
121                .build()
122                .await;
123            for error in [direct.err(), generic.err(), bip.err()] {
124                assert!(matches!(error, Some(Error::Encoding(m)) if m.contains("rpm_max_")));
125            }
126            assert!(
127                bacnet_objects::DeviceIdentity::default()
128                    .with_max_apdu(3)
129                    .is_err()
130            );
131        }
132    }
133}