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 {
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 default: i32,
49 pub max: i32,
50 pub min: i32,
51}
52
53impl Default for ZripLevels {
54 fn default() -> Self {
55 ZripLevels {
56 default: ZRIP_DEFAULT_LEVEL,
57 max: ZRIP_MAX_LEVEL,
58 min: ZRIP_MIN_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)]
111#[serde(rename_all = "camelCase")]
112pub struct Limits {
113 pub concurrency: IntRange,
115 pub upload_window: IntRange,
117 pub download_window: IntRange,
119 pub chunk_size: IntRange,
121 pub max_upload_size: u64,
123 pub max_retries: u64,
125 pub timeout_ms: u64,
127}
128
129impl Default for Limits {
130 fn default() -> Self {
131 Limits {
132 concurrency: IntRange {
133 default: 4,
134 max: 4,
135 min: 1,
136 },
137 upload_window: IntRange {
138 default: 8,
139 max: 8,
140 min: 1,
141 },
142 download_window: IntRange {
143 default: 4,
144 max: 4,
145 min: 1,
146 },
147 chunk_size: IntRange {
148 default: 2 * 1024 * 1024,
149 max: 8 * 1024 * 1024,
150 min: 256 * 1024,
151 },
152 max_upload_size: 20 * 1024 * 1024 * 1024,
153 max_retries: MAX_RETRIES as u64,
154 timeout_ms: 60_000,
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
161pub struct Capabilities {
162 pub protocol: String,
164 pub compression: CompressionCaps,
165 pub limits: Limits,
166}
167
168impl Default for Capabilities {
169 fn default() -> Self {
170 Capabilities {
171 protocol: protocol_header_value().to_string(),
172 compression: CompressionCaps::default(),
173 limits: Limits::default(),
174 }
175 }
176}
177
178impl Capabilities {
179 pub fn caps_hash(&self) -> String {
184 let bytes = serde_json::to_vec(self).expect("capabilities serialization cannot fail");
186 let digest = Sha256::digest(&bytes);
187 format!("sha256-{}", hex(&digest))
188 }
189
190 pub fn clamp_level(&self, level: i32) -> i32 {
192 self.compression.zrip_levels.clamp_level(level)
193 }
194
195 pub fn is_valid_level(&self, level: i32) -> bool {
197 self.compression.zrip_levels.contains(level)
198 }
199}
200
201fn hex(bytes: &[u8]) -> String {
203 const HEX: &[u8; 16] = b"0123456789abcdef";
204 let mut out = String::with_capacity(bytes.len() * 2);
205 for &b in bytes {
206 out.push(HEX[(b >> 4) as usize] as char);
207 out.push(HEX[(b & 0x0f) as usize] as char);
208 }
209 out
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::compress::{compressor_with_level, is_valid_zrip_level, negotiate_level};
216
217 #[test]
218 fn default_caps_serde_roundtrip() {
219 let caps = Capabilities::default();
220 let json = serde_json::to_string(&caps).unwrap();
221 let back: Capabilities = serde_json::from_str(&json).unwrap();
222 assert_eq!(back, caps);
223 assert!(json.contains("\"protocol\":\"libfw/1\""));
225 assert!(json.contains("\"zripLevels\""));
226 assert!(json.contains("\"uploadWindow\""));
227 assert!(json.contains("\"maxUploadSize\""));
228 assert!(json.contains("\"identity\""));
229 }
230
231 #[test]
232 fn caps_hash_is_stable_and_content_addressed() {
233 let a = Capabilities::default();
234 let b = Capabilities::default();
235 assert_eq!(a.caps_hash(), b.caps_hash());
236 assert!(a.caps_hash().starts_with("sha256-"));
237 assert_eq!(a.caps_hash().len(), "sha256-".len() + 64);
238
239 let mut changed = a.clone();
240 changed.limits.concurrency.default = 6;
241 assert_ne!(a.caps_hash(), changed.caps_hash());
242
243 let mut changed2 = a.clone();
244 changed2.compression.zrip_levels.max = 3;
245 assert_ne!(a.caps_hash(), changed2.caps_hash());
246 }
247
248 #[test]
249 fn negotiate_level_clamps_and_defaults() {
250 assert_eq!(negotiate_level(None, -8, 4, 1), 1);
252 assert_eq!(negotiate_level(Some(3), -8, 4, 1), 3);
254 assert_eq!(negotiate_level(Some(-20), -8, 4, 1), -8);
256 assert_eq!(negotiate_level(Some(99), -8, 4, 1), 4);
257 }
258
259 #[test]
260 fn zrip_level_bounds() {
261 assert!(is_valid_zrip_level(ZRIP_MIN_LEVEL));
262 assert!(is_valid_zrip_level(ZRIP_MAX_LEVEL));
263 assert!(is_valid_zrip_level(0));
264 assert!(!is_valid_zrip_level(ZRIP_MIN_LEVEL - 1));
265 assert!(!is_valid_zrip_level(ZRIP_MAX_LEVEL + 1));
266 let lv = ZripLevels::default();
267 assert_eq!(lv.clamp_level(99), ZRIP_MAX_LEVEL);
268 assert_eq!(lv.clamp_level(-99), ZRIP_MIN_LEVEL);
269 assert_eq!(lv.clamp_level(2), 2);
270 }
271
272 #[test]
273 fn limits_clamp_and_contains() {
274 let r = IntRange { min: 1, max: 16, default: 4 };
275 assert_eq!(r.clamp(0), 1);
276 assert_eq!(r.clamp(100), 16);
277 assert_eq!(r.clamp(8), 8);
278 assert!(r.contains(16));
279 assert!(!r.contains(17));
280 }
281
282 #[test]
283 fn compression_caps_supports() {
284 let caps = CompressionCaps::default();
285 assert!(caps.supports(CompressionFormat::Zrip));
286 assert!(caps.supports(CompressionFormat::None));
287
288 let identity_only = CompressionCaps {
289 formats: vec![CompressionFormat::None],
290 zrip_levels: ZripLevels::default(),
291 };
292 assert!(identity_only.supports(CompressionFormat::None));
293 assert!(!identity_only.supports(CompressionFormat::Zrip));
294 }
295
296 #[test]
297 fn compressor_with_level_roundtrip_across_range() {
298 let data: Vec<u8> = (0..200_000u32).map(|i| (i % 97) as u8).collect();
299 for level in [ZRIP_MIN_LEVEL, -1, 0, ZRIP_DEFAULT_LEVEL, 4] {
300 let mut c = compressor_with_level(CompressionFormat::Zrip, level).unwrap();
301 let mut compressed = Vec::new();
302 c.compress(&data, &mut compressed).unwrap();
303 c.finish(&mut compressed).unwrap();
304 assert!(!compressed.is_empty(), "level {level} produced no output");
305
306 let mut d = crate::compress::decompressor(CompressionFormat::Zrip);
307 let mut plain = Vec::new();
308 d.decompress(&compressed, &mut plain).unwrap();
309 d.finish(&mut plain).unwrap();
310 assert_eq!(plain, data, "roundtrip mismatch at level {level}");
311 }
312 }
313
314 #[test]
315 fn compressor_with_level_identity_ignores_level() {
316 let mut c = compressor_with_level(CompressionFormat::None, 99).unwrap();
317 let mut compressed = Vec::new();
318 c.compress(b"passthrough", &mut compressed).unwrap();
319 c.finish(&mut compressed).unwrap();
320 assert_eq!(compressed, b"passthrough");
321 }
322}