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