1use crate::compress::{CompressionFormat, ZRIP_DEFAULT_LEVEL, ZRIP_MAX_LEVEL, ZRIP_MIN_LEVEL};
16use crate::constants::{
17 MAX_RETRIES,
18 protocol_header_value,
19};
20use sha2::{Digest, Sha256};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
27pub struct IntRange {
28 pub default: i64,
29 pub max: i64,
30 pub min: i64,
31}
32
33impl IntRange {
34 pub fn clamp(&self, v: i64) -> i64 {
41 if self.min <= self.max {
42 v.clamp(self.min, self.max)
43 } else {
44 self.default
45 }
46 }
47
48 pub fn contains(&self, v: i64) -> bool {
50 (self.min..=self.max).contains(&v)
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56pub struct ZripLevels {
57 pub default: i32,
58 pub max: i32,
59 pub min: i32,
60}
61
62impl Default for ZripLevels {
63 fn default() -> Self {
64 ZripLevels {
65 default: ZRIP_DEFAULT_LEVEL,
66 max: ZRIP_MAX_LEVEL,
67 min: ZRIP_MIN_LEVEL,
68 }
69 }
70}
71
72impl ZripLevels {
73 pub fn clamp_level(&self, level: i32) -> i32 {
76 if self.min <= self.max {
77 level.clamp(self.min, self.max)
78 } else {
79 self.default
80 }
81 }
82
83 pub fn contains(&self, level: i32) -> bool {
85 (self.min..=self.max).contains(&level)
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
91pub struct CompressionCaps {
92 pub formats: Vec<CompressionFormat>,
94 #[serde(rename = "zripLevels")]
96 pub zrip_levels: ZripLevels,
97}
98
99impl Default for CompressionCaps {
100 fn default() -> Self {
101 CompressionCaps {
102 formats: vec![CompressionFormat::None, CompressionFormat::Zrip],
103 zrip_levels: ZripLevels::default(),
104 }
105 }
106}
107
108impl CompressionCaps {
109 pub fn supports(&self, format: CompressionFormat) -> bool {
111 self.formats.contains(&format)
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct Limits {
122 pub concurrency: IntRange,
124 pub upload_window: IntRange,
126 pub download_window: IntRange,
128 pub chunk_size: IntRange,
130 pub max_upload_size: u64,
138 pub max_retries: u64,
140 pub timeout_ms: u64,
142}
143
144impl Default for Limits {
145 fn default() -> Self {
146 Limits {
147 concurrency: IntRange {
148 default: 4,
149 max: 4,
150 min: 1,
151 },
152 upload_window: IntRange {
153 default: 8,
154 max: 8,
155 min: 1,
156 },
157 download_window: IntRange {
158 default: 4,
159 max: 4,
160 min: 1,
161 },
162 chunk_size: IntRange {
163 default: 2 * 1024 * 1024,
164 max: 8 * 1024 * 1024,
165 min: 256 * 1024,
166 },
167 max_upload_size: 20 * 1024 * 1024 * 1024,
168 max_retries: MAX_RETRIES as u64,
169 timeout_ms: 60_000,
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
176pub struct Capabilities {
177 pub protocol: String,
179 pub compression: CompressionCaps,
180 pub limits: Limits,
181}
182
183impl Default for Capabilities {
184 fn default() -> Self {
185 Capabilities {
186 protocol: protocol_header_value().to_string(),
187 compression: CompressionCaps::default(),
188 limits: Limits::default(),
189 }
190 }
191}
192
193impl Capabilities {
194 pub fn caps_hash(&self) -> String {
199 let bytes = serde_json::to_vec(self).expect("capabilities serialization cannot fail");
201 let digest = Sha256::digest(&bytes);
202 format!("sha256-{}", hex(&digest))
203 }
204
205 pub fn clamp_level(&self, level: i32) -> i32 {
207 self.compression.zrip_levels.clamp_level(level)
208 }
209
210 pub fn is_valid_level(&self, level: i32) -> bool {
212 self.compression.zrip_levels.contains(level)
213 }
214}
215
216fn hex(bytes: &[u8]) -> String {
218 const HEX: &[u8; 16] = b"0123456789abcdef";
219 let mut out = String::with_capacity(bytes.len() * 2);
220 for &b in bytes {
221 out.push(HEX[(b >> 4) as usize] as char);
222 out.push(HEX[(b & 0x0f) as usize] as char);
223 }
224 out
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::compress::{compressor_with_level, is_valid_zrip_level, negotiate_level};
231
232 #[test]
233 fn default_caps_serde_roundtrip() {
234 let caps = Capabilities::default();
235 let json = serde_json::to_string(&caps).unwrap();
236 let back: Capabilities = serde_json::from_str(&json).unwrap();
237 assert_eq!(back, caps);
238 assert!(json.contains("\"protocol\":\"libfw/1\""));
240 assert!(json.contains("\"zripLevels\""));
241 assert!(json.contains("\"uploadWindow\""));
242 assert!(json.contains("\"maxUploadSize\""));
243 assert!(json.contains("\"identity\""));
244 }
245
246 #[test]
247 fn caps_hash_is_stable_and_content_addressed() {
248 let a = Capabilities::default();
249 let b = Capabilities::default();
250 assert_eq!(a.caps_hash(), b.caps_hash());
251 assert!(a.caps_hash().starts_with("sha256-"));
252 assert_eq!(a.caps_hash().len(), "sha256-".len() + 64);
253
254 let mut changed = a.clone();
255 changed.limits.concurrency.default = 6;
256 assert_ne!(a.caps_hash(), changed.caps_hash());
257
258 let mut changed2 = a.clone();
259 changed2.compression.zrip_levels.max = 3;
260 assert_ne!(a.caps_hash(), changed2.caps_hash());
261 }
262
263 #[test]
264 fn negotiate_level_clamps_and_defaults() {
265 assert_eq!(negotiate_level(None, -8, 4, 1), 1);
267 assert_eq!(negotiate_level(Some(3), -8, 4, 1), 3);
269 assert_eq!(negotiate_level(Some(-20), -8, 4, 1), -8);
271 assert_eq!(negotiate_level(Some(99), -8, 4, 1), 4);
272 assert_eq!(negotiate_level(Some(1), 4, -8, 1), 1);
274 assert_eq!(negotiate_level(None, 4, -8, 1), 1);
275 }
276
277 #[test]
278 fn zrip_level_bounds() {
279 assert!(is_valid_zrip_level(ZRIP_MIN_LEVEL));
280 assert!(is_valid_zrip_level(ZRIP_MAX_LEVEL));
281 assert!(is_valid_zrip_level(0));
282 assert!(!is_valid_zrip_level(ZRIP_MIN_LEVEL - 1));
283 assert!(!is_valid_zrip_level(ZRIP_MAX_LEVEL + 1));
284 let lv = ZripLevels::default();
285 assert_eq!(lv.clamp_level(99), ZRIP_MAX_LEVEL);
286 assert_eq!(lv.clamp_level(-99), ZRIP_MIN_LEVEL);
287 assert_eq!(lv.clamp_level(2), 2);
288 }
289
290 #[test]
291 fn limits_clamp_and_contains() {
292 let r = IntRange { min: 1, max: 16, default: 4 };
293 assert_eq!(r.clamp(0), 1);
294 assert_eq!(r.clamp(100), 16);
295 assert_eq!(r.clamp(8), 8);
296 assert!(r.contains(16));
297 assert!(!r.contains(17));
298
299 let inverted = IntRange {
302 min: 16,
303 max: 1,
304 default: 4,
305 };
306 assert_eq!(inverted.clamp(8), 4);
307 assert_eq!(inverted.clamp(1_000_000), 4);
308 assert!(!inverted.contains(8));
309 }
310
311 #[test]
312 fn compression_caps_supports() {
313 let caps = CompressionCaps::default();
314 assert!(caps.supports(CompressionFormat::Zrip));
315 assert!(caps.supports(CompressionFormat::None));
316
317 let identity_only = CompressionCaps {
318 formats: vec![CompressionFormat::None],
319 zrip_levels: ZripLevels::default(),
320 };
321 assert!(identity_only.supports(CompressionFormat::None));
322 assert!(!identity_only.supports(CompressionFormat::Zrip));
323 }
324
325 #[test]
326 fn compressor_with_level_roundtrip_across_range() {
327 let data: Vec<u8> = (0..200_000u32).map(|i| (i % 97) as u8).collect();
328 for level in [ZRIP_MIN_LEVEL, -1, 0, ZRIP_DEFAULT_LEVEL, 4] {
329 let mut c = compressor_with_level(CompressionFormat::Zrip, level).unwrap();
330 let mut compressed = Vec::new();
331 c.compress(&data, &mut compressed).unwrap();
332 c.finish(&mut compressed).unwrap();
333 assert!(!compressed.is_empty(), "level {level} produced no output");
334
335 let mut d = crate::compress::decompressor(CompressionFormat::Zrip);
336 let mut plain = Vec::new();
337 d.decompress(&compressed, &mut plain).unwrap();
338 d.finish(&mut plain).unwrap();
339 assert_eq!(plain, data, "roundtrip mismatch at level {level}");
340 }
341 }
342
343 #[test]
344 fn compressor_with_level_identity_ignores_level() {
345 let mut c = compressor_with_level(CompressionFormat::None, 99).unwrap();
346 let mut compressed = Vec::new();
347 c.compress(b"passthrough", &mut compressed).unwrap();
348 c.finish(&mut compressed).unwrap();
349 assert_eq!(compressed, b"passthrough");
350 }
351}