Skip to main content

bacnet_server/server/
read_range_budget.rs

1//! ReadRange response-page limits, not total list-read work limits.
2use super::*;
3
4/// Positive limits on returned items and the complete logical service ACK.
5/// APDU/NPDU envelopes are excluded. Unsegmented peers may further reduce bytes.
6/// Full property reads, identity retrieval/selection, and one trial item encoding
7/// remain outside these limits.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct ReadRangeBudget {
10    /// Maximum items per directional page (default 256).
11    pub max_returned_items: usize,
12    /// Maximum complete service-ACK bytes (default 16384).
13    pub max_service_ack_bytes: usize,
14}
15
16impl Default for ReadRangeBudget {
17    fn default() -> Self {
18        Self {
19            max_returned_items: 256,
20            max_service_ack_bytes: 16384,
21        }
22    }
23}
24
25impl ReadRangeBudget {
26    /// Reject zero configuration before startup or SC dialing.
27    pub fn validate(&self) -> Result<(), Error> {
28        for (name, value) in [
29            ("read_range_max_returned_items", self.max_returned_items),
30            (
31                "read_range_max_service_ack_bytes",
32                self.max_service_ack_bytes,
33            ),
34        ] {
35            if value == 0 {
36                return Err(Error::Encoding(format!("{name} must be positive")));
37            }
38        }
39        Ok(())
40    }
41}
42
43impl<T: TransportPort + 'static> ServerBuilder<T> {
44    /// Set ReadRange page limits, validated before startup.
45    pub fn read_range_budget(mut self, budget: ReadRangeBudget) -> Self {
46        self.config.read_range_budget = budget;
47        self
48    }
49}
50impl BipServerBuilder {
51    /// Set ReadRange page limits, validated before startup.
52    pub fn read_range_budget(mut self, budget: ReadRangeBudget) -> Self {
53        self.config.read_range_budget = budget;
54        self
55    }
56}
57#[cfg(feature = "sc-tls")]
58impl ScServerBuilder {
59    /// Set ReadRange page limits, validated before SC dialing.
60    pub fn read_range_budget(mut self, budget: ReadRangeBudget) -> Self {
61        self.config.read_range_budget = budget;
62        self
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::server::test_transport::TestTransport;
70
71    #[tokio::test]
72    async fn read_range_all_builders_validate_before_start_or_dial() {
73        let default = ReadRangeBudget::default();
74        assert_eq!(
75            (default.max_returned_items, default.max_service_ack_bytes),
76            (256, 16384)
77        );
78        assert_eq!(ServerConfig::default().read_range_budget, default);
79        assert!(format!("{:?}", ServerConfig::default()).contains("read_range_budget"));
80        ReadRangeBudget {
81            max_returned_items: usize::MAX,
82            max_service_ack_bytes: usize::MAX,
83        }
84        .validate()
85        .unwrap();
86        for budget in [
87            ReadRangeBudget {
88                max_returned_items: 0,
89                ..default
90            },
91            ReadRangeBudget {
92                max_service_ack_bytes: 0,
93                ..default
94            },
95        ] {
96            let direct = BACnetServer::start(
97                ServerConfig {
98                    read_range_budget: budget,
99                    ..Default::default()
100                },
101                ObjectDatabase::new(),
102                TestTransport::never_start(),
103            )
104            .await
105            .err();
106            let generic = BACnetServer::generic_builder()
107                .transport(TestTransport::never_start())
108                .read_range_budget(budget)
109                .build()
110                .await
111                .err();
112            let bip = BACnetServer::bip_builder()
113                .port(0)
114                .read_range_budget(budget)
115                .build()
116                .await
117                .err();
118            for error in [direct, generic, bip] {
119                assert!(matches!(error, Some(Error::Encoding(m)) if m.contains("read_range_max_")));
120            }
121            #[cfg(feature = "sc-tls")]
122            {
123                let error = BACnetServer::sc_builder()
124                    .hub_url("not-a-websocket-url")
125                    .tls_config(crate::server::sc_builder::test_tls_config())
126                    .device_uuid(crate::server::sc_builder::TEST_DEVICE_UUID)
127                    .read_range_budget(budget)
128                    .build()
129                    .await
130                    .err();
131                assert!(matches!(error, Some(Error::Encoding(m)) if m.contains("read_range_max_")));
132            }
133        }
134    }
135}