1use crate::error::{ServiceError, ERROR_BAD_CHUNK, ERROR_REQUEST_TOO_LARGE};
36use serde_json::{json, Value};
37use std::time::{Duration, Instant};
38
39pub const METHOD_UPLOAD: &str = "rk.upload";
41pub const METHOD_FETCH: &str = "rk.fetch";
43pub const RESERVED_PREFIX: &str = "rk.";
45
46pub const CHUNK_TEXT_BYTES: usize = 1024 * 1024;
48pub const DEFAULT_MAX_MESSAGE: usize = 0;
51pub const SUGGESTED_MAX_MESSAGE: usize = 64 * 1024 * 1024;
53
54pub(crate) fn chunk_bytes_for(frame_limit: usize) -> usize {
57 (frame_limit / 4).clamp(16, CHUNK_TEXT_BYTES)
58}
59
60pub(crate) fn boundaries(text: &str, chunk: usize) -> Vec<usize> {
63 let chunk = chunk.max(4);
64 let mut ends = Vec::new();
65 let mut start = 0;
66 while start < text.len() {
67 let mut end = (start + chunk).min(text.len());
68 while !text.is_char_boundary(end) {
70 end += 1;
71 }
72 ends.push(end);
73 start = end;
74 }
75 if ends.is_empty() {
76 ends.push(0);
77 }
78 ends
79}
80
81fn bad(message: impl Into<String>) -> ServiceError {
82 ServiceError::new(ERROR_BAD_CHUNK, message)
83}
84
85pub const MAX_ASSEMBLIES_PER_CONNECTION: usize = 1;
88pub const UPLOAD_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
91pub const MAX_JSON_DEPTH: usize = 64;
93pub const MAX_JSON_NODES: usize = 1 << 20;
97
98pub(crate) fn check_shape(text: &str) -> Result<(), ServiceError> {
102 let (mut depth, mut nodes) = (0usize, 1usize);
103 let (mut in_string, mut escaped) = (false, false);
104 for &b in text.as_bytes() {
105 if in_string {
106 if escaped {
107 escaped = false;
108 } else if b == b'\\' {
109 escaped = true;
110 } else if b == b'"' {
111 in_string = false;
112 }
113 continue;
114 }
115 match b {
116 b'"' => in_string = true,
117 b'[' | b'{' => {
118 depth += 1;
119 nodes += 1;
120 if depth > MAX_JSON_DEPTH {
121 return Err(bad(format!(
122 "the assembled params nest deeper than {MAX_JSON_DEPTH} levels"
123 )));
124 }
125 }
126 b']' | b'}' => depth = depth.saturating_sub(1),
127 b',' => nodes += 1,
128 _ => {}
129 }
130 if nodes > MAX_JSON_NODES {
131 return Err(ServiceError::new(
132 ERROR_REQUEST_TOO_LARGE,
133 format!("the assembled params hold more than {MAX_JSON_NODES} values"),
134 ));
135 }
136 }
137 Ok(())
138}
139
140#[derive(Debug)]
142struct Upload {
143 id: String,
144 method: String,
145 next: u64,
146 text: String,
147 touched: Instant,
148}
149
150#[derive(Debug, PartialEq)]
152pub(crate) enum UploadStep {
153 More { received: u64 },
155 Done { method: String, params: Value },
157}
158
159#[derive(Debug, Default)]
161pub(crate) struct Uploads {
162 current: Option<Upload>,
163}
164
165impl Uploads {
166 #[cfg(test)]
168 pub(crate) fn buffered_bytes(&self) -> usize {
169 self.current.as_ref().map_or(0, |u| u.text.len())
170 }
171
172 pub(crate) fn expire(&mut self) {
174 if self
175 .current
176 .as_ref()
177 .is_some_and(|u| u.touched.elapsed() > UPLOAD_IDLE_TIMEOUT)
178 {
179 self.current = None;
180 }
181 }
182
183 #[cfg(test)]
185 pub(crate) fn accept(
186 &mut self,
187 params: &Value,
188 max: usize,
189 ) -> Result<UploadStep, ServiceError> {
190 self.accept_with(params, max, &|_| Ok(()))
191 }
192
193 pub(crate) fn accept_with(
198 &mut self,
199 params: &Value,
200 max: usize,
201 authorize: &dyn Fn(&str) -> Result<(), ServiceError>,
202 ) -> Result<UploadStep, ServiceError> {
203 self.expire();
204 let result = self.accept_inner(params, max, authorize);
205 if result.is_err() {
206 self.current = None;
207 }
208 result
209 }
210
211 fn accept_inner(
212 &mut self,
213 params: &Value,
214 max: usize,
215 authorize: &dyn Fn(&str) -> Result<(), ServiceError>,
216 ) -> Result<UploadStep, ServiceError> {
217 let id = params["id"]
218 .as_str()
219 .filter(|s| !s.is_empty() && s.len() <= 64)
220 .ok_or_else(|| bad("an upload piece needs an id of 1 to 64 characters"))?;
221 let seq = params["seq"]
222 .as_u64()
223 .ok_or_else(|| bad("an upload piece needs a numeric seq"))?;
224 let last = params["last"].as_bool().unwrap_or(false);
225 let text = params["text"]
226 .as_str()
227 .ok_or_else(|| bad("an upload piece needs a text"))?;
228 if seq == 0 {
229 let method = params["method"]
230 .as_str()
231 .filter(|m| !m.is_empty() && !m.starts_with(RESERVED_PREFIX))
232 .ok_or_else(|| bad("the first piece must name a method outside the rk. prefix"))?;
233 self.current = None;
236 authorize(method)?;
237 if text.len() > max {
238 return Err(ServiceError::new(
239 ERROR_REQUEST_TOO_LARGE,
240 format!("the message is larger than the {max}-byte message limit"),
241 ));
242 }
243 self.current = Some(Upload {
244 id: id.to_string(),
245 method: method.to_string(),
246 next: 0,
247 text: String::new(),
248 touched: Instant::now(),
249 });
250 }
251 let up = match self.current.as_mut() {
252 Some(up) if up.id == id && up.next == seq => up,
253 _ => {
254 return Err(bad(
255 "upload piece out of order, repeated, or for another upload",
256 ))
257 }
258 };
259 if up.text.len().saturating_add(text.len()) > max {
260 return Err(ServiceError::new(
261 ERROR_REQUEST_TOO_LARGE,
262 format!("the message is larger than the {max}-byte message limit"),
263 ));
264 }
265 up.text.push_str(text);
266 up.next += 1;
267 up.touched = Instant::now();
268 if !last {
269 return Ok(UploadStep::More { received: seq });
270 }
271 let up = self.current.take().expect("the upload was just extended");
272 check_shape(&up.text)?;
275 let params = serde_json::from_str::<Value>(&up.text)
276 .map_err(|e| bad(format!("the assembled params are not JSON: {e}")))?;
277 Ok(UploadStep::Done {
278 method: up.method,
279 params,
280 })
281 }
282}
283
284#[derive(Debug)]
286pub(crate) struct Download {
287 id: String,
288 text: String,
289 ends: Vec<usize>,
290}
291
292impl Download {
293 pub(crate) fn new(id: String, text: String, chunk: usize) -> Self {
294 let ends = boundaries(&text, chunk);
295 Self { id, text, ends }
296 }
297
298 pub(crate) fn descriptor(&self) -> Value {
300 json!({"id": self.id, "bytes": self.text.len(), "count": self.ends.len()})
301 }
302
303 pub(crate) fn fetch(&self, params: &Value) -> Result<Value, ServiceError> {
305 if params["id"].as_str() != Some(self.id.as_str()) {
306 return Err(bad(
307 "no such result on this connection (it was replaced or released)",
308 ));
309 }
310 let seq = params["seq"]
311 .as_u64()
312 .and_then(|n| usize::try_from(n).ok())
313 .filter(|n| *n < self.ends.len())
314 .ok_or_else(|| bad("fetch seq is out of range"))?;
315 let start = if seq == 0 { 0 } else { self.ends[seq - 1] };
316 Ok(json!({"seq": seq, "text": &self.text[start..self.ends[seq]]}))
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 fn piece(id: &str, seq: u64, last: bool, text: &str, method: Option<&str>) -> Value {
325 let mut p = json!({"id": id, "seq": seq, "last": last, "text": text});
326 if let Some(m) = method {
327 p["method"] = json!(m);
328 }
329 p
330 }
331
332 #[test]
333 fn boundaries_cover_the_text_and_never_split_a_character() {
334 let text = "aé€😀".repeat(50);
335 for chunk in [4, 5, 7, 64, 1000] {
336 let ends = boundaries(&text, chunk);
337 assert_eq!(*ends.last().unwrap(), text.len());
338 let mut start = 0;
339 for end in &ends {
340 assert!(text.is_char_boundary(*end));
341 assert!(*end > start);
342 start = *end;
343 }
344 }
345 assert_eq!(boundaries("", 8), vec![0]);
346 }
347
348 #[test]
349 fn an_upload_assembles_in_order_and_yields_the_params() {
350 let whole = json!({"a": "x".repeat(100), "b": [1, 2, 3]});
351 let text = serde_json::to_string(&whole).unwrap();
352 let ends = boundaries(&text, 30);
353 let mut up = Uploads::default();
354 let mut start = 0;
355 for (i, end) in ends.iter().enumerate() {
356 let last = i + 1 == ends.len();
357 let p = piece(
358 "u1",
359 i as u64,
360 last,
361 &text[start..*end],
362 (i == 0).then_some("m.big"),
363 );
364 start = *end;
365 match up.accept(&p, 1 << 20).unwrap() {
366 UploadStep::More { received } => {
367 assert!(!last);
368 assert_eq!(received, i as u64);
369 }
370 UploadStep::Done { method, params } => {
371 assert!(last);
372 assert_eq!(method, "m.big");
373 assert_eq!(params, whole);
374 }
375 }
376 }
377 }
378
379 #[test]
380 fn a_gap_a_repeat_or_a_foreign_id_drops_the_upload() {
381 let mut up = Uploads::default();
382 up.accept(&piece("u1", 0, false, "{", Some("m")), 1000)
383 .unwrap();
384 assert!(up.accept(&piece("u1", 2, false, "x", None), 1000).is_err());
385 assert!(up.accept(&piece("u1", 1, false, "x", None), 1000).is_err());
387
388 up.accept(&piece("u2", 0, false, "{", Some("m")), 1000)
389 .unwrap();
390 assert!(up.accept(&piece("u2", 0, false, "{", None), 1000).is_err());
391 up.accept(&piece("u3", 0, false, "{", Some("m")), 1000)
392 .unwrap();
393 assert!(up
394 .accept(&piece("other", 1, false, "x", None), 1000)
395 .is_err());
396 }
397
398 #[test]
399 fn the_message_bound_and_the_reserved_prefix_are_enforced() {
400 let mut up = Uploads::default();
401 up.accept(&piece("u", 0, false, "12345", Some("m")), 8)
402 .unwrap();
403 let err = up
404 .accept(&piece("u", 1, false, "6789", None), 8)
405 .unwrap_err();
406 assert_eq!(err.code, ERROR_REQUEST_TOO_LARGE);
407 assert!(up
408 .accept(&piece("v", 0, false, "{", Some("rk.fetch")), 8)
409 .is_err());
410 assert!(up.accept(&piece("v", 0, false, "{", None), 8).is_err());
411 }
412
413 #[test]
414 fn assembled_text_that_is_not_json_is_refused() {
415 let mut up = Uploads::default();
416 let err = up
417 .accept(&piece("u", 0, true, "{not json", Some("m")), 1000)
418 .unwrap_err();
419 assert_eq!(err.code, ERROR_BAD_CHUNK);
420 }
421
422 #[test]
423 fn the_first_piece_is_authorised_before_anything_is_buffered() {
424 let mut up = Uploads::default();
425 let deny = |_: &str| Err(ServiceError::new(crate::error::ERROR_NOT_ALLOWED, "no"));
426 let err = up
427 .accept_with(&piece("u", 0, false, "{\"a\":1", Some("m")), 1000, &deny)
428 .unwrap_err();
429 assert_eq!(err.code, crate::error::ERROR_NOT_ALLOWED);
430 assert_eq!(up.buffered_bytes(), 0);
431 let err = up
433 .accept_with(&piece("u", 1, true, "}", None), 1000, &|_| Ok(()))
434 .unwrap_err();
435 assert_eq!(err.code, ERROR_BAD_CHUNK);
436 up.accept(&piece("v", 0, false, "{", Some("m")), 1000)
438 .unwrap();
439 assert_eq!(up.buffered_bytes(), 1);
440 assert!(up
441 .accept_with(&piece("w", 0, false, "{", Some("m")), 1000, &deny)
442 .is_err());
443 assert_eq!(up.buffered_bytes(), 0);
444 }
445
446 #[test]
447 fn deep_or_dense_bodies_are_refused_before_the_parse() {
448 let deep = format!(
449 "{}{}",
450 "[".repeat(MAX_JSON_DEPTH + 1),
451 "]".repeat(MAX_JSON_DEPTH + 1)
452 );
453 assert_eq!(check_shape(&deep).unwrap_err().code, ERROR_BAD_CHUNK);
454 let ok = format!(
455 "{}{}",
456 "[".repeat(MAX_JSON_DEPTH),
457 "]".repeat(MAX_JSON_DEPTH)
458 );
459 assert!(check_shape(&ok).is_ok());
460 let dense = format!("[{}0]", "0,".repeat(MAX_JSON_NODES));
461 assert_eq!(
462 check_shape(&dense).unwrap_err().code,
463 ERROR_REQUEST_TOO_LARGE
464 );
465 let text = serde_json::to_string(&json!({"s": "[[[[,,,,\"]]".repeat(50_000)})).unwrap();
467 assert!(check_shape(&text).is_ok());
468
469 let mut up = Uploads::default();
470 let err = up
471 .accept(&piece("u", 0, true, &deep, Some("m")), 1 << 20)
472 .unwrap_err();
473 assert_eq!(err.code, ERROR_BAD_CHUNK);
474 assert_eq!(up.buffered_bytes(), 0);
475 }
476
477 #[test]
478 fn an_idle_assembly_expires_and_a_failure_frees_it() {
479 let mut up = Uploads::default();
480 up.accept(&piece("u", 0, false, "{", Some("m")), 1000)
481 .unwrap();
482 assert_eq!(up.buffered_bytes(), 1);
483 let Some(stale) = Instant::now().checked_sub(UPLOAD_IDLE_TIMEOUT + Duration::from_secs(1))
484 else {
485 return; };
487 up.current.as_mut().unwrap().touched = stale;
488 up.expire();
489 assert_eq!(up.buffered_bytes(), 0);
490 up.accept(&piece("u", 0, false, "{", Some("m")), 1000)
491 .unwrap();
492 assert!(up.accept(&piece("u", 5, false, "x", None), 1000).is_err());
493 assert_eq!(up.buffered_bytes(), 0);
494 }
495
496 #[test]
497 fn chunking_is_off_by_default() {
498 assert_eq!(DEFAULT_MAX_MESSAGE, 0);
499 }
500
501 #[test]
502 fn a_download_serves_every_piece_and_rejects_strangers() {
503 let text = "héllo wörld ".repeat(40);
504 let dl = Download::new("d1".into(), text.clone(), 25);
505 let desc = dl.descriptor();
506 assert_eq!(desc["bytes"], text.len());
507 let count = desc["count"].as_u64().unwrap();
508 let mut joined = String::new();
509 for seq in 0..count {
510 let r = dl.fetch(&json!({"id": "d1", "seq": seq})).unwrap();
511 joined.push_str(r["text"].as_str().unwrap());
512 }
513 assert_eq!(joined, text);
514 assert!(dl.fetch(&json!({"id": "d1", "seq": count})).is_err());
515 assert!(dl.fetch(&json!({"id": "d2", "seq": 0})).is_err());
516 }
517
518 #[test]
519 fn the_chunk_size_scales_with_the_frame_limit() {
520 assert_eq!(chunk_bytes_for(16 * 1024 * 1024), CHUNK_TEXT_BYTES);
521 assert_eq!(chunk_bytes_for(4096), 1024);
522 assert_eq!(chunk_bytes_for(10), 16);
523 }
524}