Skip to main content

rullst_core/client_contract/
policy.rs

1use super::{
2    CURRENT_CLIENT_CONTRACT_VERSION, ClientContractError, ClientRequest, ClientVersionOffer,
3    ContractVersion, DEFAULT_CLIENT_CONTRACT_BODY_BYTES, MAX_CLIENT_CONTRACT_BODY_BYTES,
4    ServerFailure, ServerResponse,
5};
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8use std::io::Write;
9
10/// Server policy for version negotiation and bounded JSON encoding/decoding.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub struct ClientContractPolicy {
14    minimum_version: ContractVersion,
15    current_version: ContractVersion,
16    max_body_bytes: usize,
17}
18
19impl ClientContractPolicy {
20    /// Creates a policy with an inclusive supported version range and body limit.
21    pub const fn new(
22        minimum_version: ContractVersion,
23        current_version: ContractVersion,
24        max_body_bytes: usize,
25    ) -> Result<Self, ClientContractError> {
26        if minimum_version.get() > current_version.get()
27            || max_body_bytes == 0
28            || max_body_bytes > MAX_CLIENT_CONTRACT_BODY_BYTES
29        {
30            return Err(ClientContractError::InvalidPolicy);
31        }
32        Ok(Self {
33            minimum_version,
34            current_version,
35            max_body_bytes,
36        })
37    }
38
39    /// Returns the minimum accepted wire version.
40    pub const fn minimum_version(self) -> ContractVersion {
41        self.minimum_version
42    }
43
44    /// Returns the latest emitted wire version.
45    pub const fn current_version(self) -> ContractVersion {
46        self.current_version
47    }
48
49    /// Returns the configured encoded-body ceiling.
50    pub const fn max_body_bytes(self) -> usize {
51        self.max_body_bytes
52    }
53
54    /// Decodes a bounded client version offer.
55    pub fn decode_offer(self, body: &[u8]) -> Result<ClientVersionOffer, ClientContractError> {
56        self.validate_body_size(body.len())?;
57        serde_json::from_slice(body).map_err(ClientContractError::InvalidJson)
58    }
59
60    /// Encodes a client version offer within the configured ceiling.
61    pub fn encode_offer(self, offer: &ClientVersionOffer) -> Result<Vec<u8>, ClientContractError> {
62        self.encode_bounded(offer)
63    }
64
65    /// Selects the highest mutually supported version.
66    pub fn negotiate(
67        self,
68        offer: &ClientVersionOffer,
69    ) -> Result<ContractVersion, ClientContractError> {
70        offer
71            .supported_versions()
72            .iter()
73            .rev()
74            .copied()
75            .find(|version| *version >= self.minimum_version && *version <= self.current_version)
76            .ok_or(ClientContractError::NoMutualVersion)
77    }
78
79    /// Verifies that a version falls inside the configured inclusive range.
80    pub const fn validate_version(
81        self,
82        version: ContractVersion,
83    ) -> Result<(), ClientContractError> {
84        if version.get() < self.minimum_version.get() || version.get() > self.current_version.get()
85        {
86            Err(ClientContractError::UnsupportedVersion {
87                received: version.get(),
88                minimum: self.minimum_version.get(),
89                current: self.current_version.get(),
90            })
91        } else {
92            Ok(())
93        }
94    }
95
96    /// Decodes and validates a bounded client request.
97    pub fn decode_request<T>(self, body: &[u8]) -> Result<ClientRequest<T>, ClientContractError>
98    where
99        T: DeserializeOwned,
100    {
101        self.validate_body_size(body.len())?;
102        let request = serde_json::from_slice::<ClientRequest<T>>(body)
103            .map_err(ClientContractError::InvalidJson)?;
104        self.validate_version(request.version())?;
105        Ok(request)
106    }
107
108    /// Encodes a validated client request within the configured ceiling.
109    pub fn encode_request<T>(
110        self,
111        request: &ClientRequest<T>,
112    ) -> Result<Vec<u8>, ClientContractError>
113    where
114        T: Serialize,
115    {
116        self.validate_version(request.version())?;
117        self.encode_bounded(request)
118    }
119
120    /// Decodes and validates a bounded success response.
121    pub fn decode_response<T>(self, body: &[u8]) -> Result<ServerResponse<T>, ClientContractError>
122    where
123        T: DeserializeOwned,
124    {
125        self.validate_body_size(body.len())?;
126        let response = serde_json::from_slice::<ServerResponse<T>>(body)
127            .map_err(ClientContractError::InvalidJson)?;
128        self.validate_version(response.version())?;
129        Ok(response)
130    }
131
132    /// Encodes a success response within the configured ceiling.
133    pub fn encode_response<T>(
134        self,
135        response: &ServerResponse<T>,
136    ) -> Result<Vec<u8>, ClientContractError>
137    where
138        T: Serialize,
139    {
140        self.validate_version(response.version())?;
141        self.encode_bounded(response)
142    }
143
144    /// Decodes and validates a bounded failure response.
145    pub fn decode_failure(self, body: &[u8]) -> Result<ServerFailure, ClientContractError> {
146        self.validate_body_size(body.len())?;
147        let response = serde_json::from_slice::<ServerFailure>(body)
148            .map_err(ClientContractError::InvalidJson)?;
149        self.validate_version(response.version())?;
150        Ok(response)
151    }
152
153    /// Encodes a failure response within the configured ceiling.
154    pub fn encode_failure(self, response: &ServerFailure) -> Result<Vec<u8>, ClientContractError> {
155        self.validate_version(response.version())?;
156        self.encode_bounded(response)
157    }
158
159    fn encode_bounded<T>(self, value: &T) -> Result<Vec<u8>, ClientContractError>
160    where
161        T: Serialize,
162    {
163        let mut writer = BoundedJsonWriter::new(self.max_body_bytes);
164        match serde_json::to_writer(&mut writer, value) {
165            Ok(()) => Ok(writer.into_bytes()),
166            Err(_) if writer.exceeded() => Err(ClientContractError::BodyTooLarge {
167                maximum: self.max_body_bytes,
168            }),
169            Err(error) => Err(ClientContractError::EncodeJson(error)),
170        }
171    }
172
173    const fn validate_body_size(self, received: usize) -> Result<(), ClientContractError> {
174        if received > self.max_body_bytes {
175            Err(ClientContractError::BodyTooLarge {
176                maximum: self.max_body_bytes,
177            })
178        } else {
179            Ok(())
180        }
181    }
182}
183
184struct BoundedJsonWriter {
185    bytes: Vec<u8>,
186    maximum: usize,
187    exceeded: bool,
188}
189
190impl BoundedJsonWriter {
191    fn new(maximum: usize) -> Self {
192        Self {
193            bytes: Vec::new(),
194            maximum,
195            exceeded: false,
196        }
197    }
198
199    fn exceeded(&self) -> bool {
200        self.exceeded
201    }
202
203    fn into_bytes(self) -> Vec<u8> {
204        self.bytes
205    }
206}
207
208impl Write for BoundedJsonWriter {
209    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
210        let remaining = self.maximum.saturating_sub(self.bytes.len());
211        if buffer.len() > remaining {
212            self.exceeded = true;
213            return Err(std::io::Error::other("client contract body limit reached"));
214        }
215        self.bytes.extend_from_slice(buffer);
216        Ok(buffer.len())
217    }
218
219    fn flush(&mut self) -> std::io::Result<()> {
220        Ok(())
221    }
222}
223
224impl Default for ClientContractPolicy {
225    fn default() -> Self {
226        Self {
227            minimum_version: CURRENT_CLIENT_CONTRACT_VERSION,
228            current_version: CURRENT_CLIENT_CONTRACT_VERSION,
229            max_body_bytes: DEFAULT_CLIENT_CONTRACT_BODY_BYTES,
230        }
231    }
232}