use crate::error::{ServiceError, ERROR_BAD_CHUNK, ERROR_REQUEST_TOO_LARGE};
use serde_json::{json, Value};
use std::time::{Duration, Instant};
pub const METHOD_UPLOAD: &str = "rk.upload";
pub const METHOD_FETCH: &str = "rk.fetch";
pub const RESERVED_PREFIX: &str = "rk.";
pub const CHUNK_TEXT_BYTES: usize = 1024 * 1024;
pub const DEFAULT_MAX_MESSAGE: usize = 0;
pub const SUGGESTED_MAX_MESSAGE: usize = 64 * 1024 * 1024;
pub(crate) fn chunk_bytes_for(frame_limit: usize) -> usize {
(frame_limit / 4).clamp(16, CHUNK_TEXT_BYTES)
}
pub(crate) fn boundaries(text: &str, chunk: usize) -> Vec<usize> {
let chunk = chunk.max(4);
let mut ends = Vec::new();
let mut start = 0;
while start < text.len() {
let mut end = (start + chunk).min(text.len());
while !text.is_char_boundary(end) {
end += 1;
}
ends.push(end);
start = end;
}
if ends.is_empty() {
ends.push(0);
}
ends
}
fn bad(message: impl Into<String>) -> ServiceError {
ServiceError::new(ERROR_BAD_CHUNK, message)
}
pub const MAX_ASSEMBLIES_PER_CONNECTION: usize = 1;
pub const UPLOAD_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
pub const MAX_JSON_DEPTH: usize = 64;
pub const MAX_JSON_NODES: usize = 1 << 20;
pub(crate) fn check_shape(text: &str) -> Result<(), ServiceError> {
let (mut depth, mut nodes) = (0usize, 1usize);
let (mut in_string, mut escaped) = (false, false);
for &b in text.as_bytes() {
if in_string {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == b'"' {
in_string = false;
}
continue;
}
match b {
b'"' => in_string = true,
b'[' | b'{' => {
depth += 1;
nodes += 1;
if depth > MAX_JSON_DEPTH {
return Err(bad(format!(
"the assembled params nest deeper than {MAX_JSON_DEPTH} levels"
)));
}
}
b']' | b'}' => depth = depth.saturating_sub(1),
b',' => nodes += 1,
_ => {}
}
if nodes > MAX_JSON_NODES {
return Err(ServiceError::new(
ERROR_REQUEST_TOO_LARGE,
format!("the assembled params hold more than {MAX_JSON_NODES} values"),
));
}
}
Ok(())
}
#[derive(Debug)]
struct Upload {
id: String,
method: String,
next: u64,
text: String,
touched: Instant,
}
#[derive(Debug, PartialEq)]
pub(crate) enum UploadStep {
More { received: u64 },
Done { method: String, params: Value },
}
#[derive(Debug, Default)]
pub(crate) struct Uploads {
current: Option<Upload>,
}
impl Uploads {
#[cfg(test)]
pub(crate) fn buffered_bytes(&self) -> usize {
self.current.as_ref().map_or(0, |u| u.text.len())
}
pub(crate) fn expire(&mut self) {
if self
.current
.as_ref()
.is_some_and(|u| u.touched.elapsed() > UPLOAD_IDLE_TIMEOUT)
{
self.current = None;
}
}
#[cfg(test)]
pub(crate) fn accept(
&mut self,
params: &Value,
max: usize,
) -> Result<UploadStep, ServiceError> {
self.accept_with(params, max, &|_| Ok(()))
}
pub(crate) fn accept_with(
&mut self,
params: &Value,
max: usize,
authorize: &dyn Fn(&str) -> Result<(), ServiceError>,
) -> Result<UploadStep, ServiceError> {
self.expire();
let result = self.accept_inner(params, max, authorize);
if result.is_err() {
self.current = None;
}
result
}
fn accept_inner(
&mut self,
params: &Value,
max: usize,
authorize: &dyn Fn(&str) -> Result<(), ServiceError>,
) -> Result<UploadStep, ServiceError> {
let id = params["id"]
.as_str()
.filter(|s| !s.is_empty() && s.len() <= 64)
.ok_or_else(|| bad("an upload piece needs an id of 1 to 64 characters"))?;
let seq = params["seq"]
.as_u64()
.ok_or_else(|| bad("an upload piece needs a numeric seq"))?;
let last = params["last"].as_bool().unwrap_or(false);
let text = params["text"]
.as_str()
.ok_or_else(|| bad("an upload piece needs a text"))?;
if seq == 0 {
let method = params["method"]
.as_str()
.filter(|m| !m.is_empty() && !m.starts_with(RESERVED_PREFIX))
.ok_or_else(|| bad("the first piece must name a method outside the rk. prefix"))?;
self.current = None;
authorize(method)?;
if text.len() > max {
return Err(ServiceError::new(
ERROR_REQUEST_TOO_LARGE,
format!("the message is larger than the {max}-byte message limit"),
));
}
self.current = Some(Upload {
id: id.to_string(),
method: method.to_string(),
next: 0,
text: String::new(),
touched: Instant::now(),
});
}
let up = match self.current.as_mut() {
Some(up) if up.id == id && up.next == seq => up,
_ => {
return Err(bad(
"upload piece out of order, repeated, or for another upload",
))
}
};
if up.text.len().saturating_add(text.len()) > max {
return Err(ServiceError::new(
ERROR_REQUEST_TOO_LARGE,
format!("the message is larger than the {max}-byte message limit"),
));
}
up.text.push_str(text);
up.next += 1;
up.touched = Instant::now();
if !last {
return Ok(UploadStep::More { received: seq });
}
let up = self.current.take().expect("the upload was just extended");
check_shape(&up.text)?;
let params = serde_json::from_str::<Value>(&up.text)
.map_err(|e| bad(format!("the assembled params are not JSON: {e}")))?;
Ok(UploadStep::Done {
method: up.method,
params,
})
}
}
#[derive(Debug)]
pub(crate) struct Download {
id: String,
text: String,
ends: Vec<usize>,
}
impl Download {
pub(crate) fn new(id: String, text: String, chunk: usize) -> Self {
let ends = boundaries(&text, chunk);
Self { id, text, ends }
}
pub(crate) fn descriptor(&self) -> Value {
json!({"id": self.id, "bytes": self.text.len(), "count": self.ends.len()})
}
pub(crate) fn fetch(&self, params: &Value) -> Result<Value, ServiceError> {
if params["id"].as_str() != Some(self.id.as_str()) {
return Err(bad(
"no such result on this connection (it was replaced or released)",
));
}
let seq = params["seq"]
.as_u64()
.and_then(|n| usize::try_from(n).ok())
.filter(|n| *n < self.ends.len())
.ok_or_else(|| bad("fetch seq is out of range"))?;
let start = if seq == 0 { 0 } else { self.ends[seq - 1] };
Ok(json!({"seq": seq, "text": &self.text[start..self.ends[seq]]}))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn piece(id: &str, seq: u64, last: bool, text: &str, method: Option<&str>) -> Value {
let mut p = json!({"id": id, "seq": seq, "last": last, "text": text});
if let Some(m) = method {
p["method"] = json!(m);
}
p
}
#[test]
fn boundaries_cover_the_text_and_never_split_a_character() {
let text = "aé€😀".repeat(50);
for chunk in [4, 5, 7, 64, 1000] {
let ends = boundaries(&text, chunk);
assert_eq!(*ends.last().unwrap(), text.len());
let mut start = 0;
for end in &ends {
assert!(text.is_char_boundary(*end));
assert!(*end > start);
start = *end;
}
}
assert_eq!(boundaries("", 8), vec![0]);
}
#[test]
fn an_upload_assembles_in_order_and_yields_the_params() {
let whole = json!({"a": "x".repeat(100), "b": [1, 2, 3]});
let text = serde_json::to_string(&whole).unwrap();
let ends = boundaries(&text, 30);
let mut up = Uploads::default();
let mut start = 0;
for (i, end) in ends.iter().enumerate() {
let last = i + 1 == ends.len();
let p = piece(
"u1",
i as u64,
last,
&text[start..*end],
(i == 0).then_some("m.big"),
);
start = *end;
match up.accept(&p, 1 << 20).unwrap() {
UploadStep::More { received } => {
assert!(!last);
assert_eq!(received, i as u64);
}
UploadStep::Done { method, params } => {
assert!(last);
assert_eq!(method, "m.big");
assert_eq!(params, whole);
}
}
}
}
#[test]
fn a_gap_a_repeat_or_a_foreign_id_drops_the_upload() {
let mut up = Uploads::default();
up.accept(&piece("u1", 0, false, "{", Some("m")), 1000)
.unwrap();
assert!(up.accept(&piece("u1", 2, false, "x", None), 1000).is_err());
assert!(up.accept(&piece("u1", 1, false, "x", None), 1000).is_err());
up.accept(&piece("u2", 0, false, "{", Some("m")), 1000)
.unwrap();
assert!(up.accept(&piece("u2", 0, false, "{", None), 1000).is_err());
up.accept(&piece("u3", 0, false, "{", Some("m")), 1000)
.unwrap();
assert!(up
.accept(&piece("other", 1, false, "x", None), 1000)
.is_err());
}
#[test]
fn the_message_bound_and_the_reserved_prefix_are_enforced() {
let mut up = Uploads::default();
up.accept(&piece("u", 0, false, "12345", Some("m")), 8)
.unwrap();
let err = up
.accept(&piece("u", 1, false, "6789", None), 8)
.unwrap_err();
assert_eq!(err.code, ERROR_REQUEST_TOO_LARGE);
assert!(up
.accept(&piece("v", 0, false, "{", Some("rk.fetch")), 8)
.is_err());
assert!(up.accept(&piece("v", 0, false, "{", None), 8).is_err());
}
#[test]
fn assembled_text_that_is_not_json_is_refused() {
let mut up = Uploads::default();
let err = up
.accept(&piece("u", 0, true, "{not json", Some("m")), 1000)
.unwrap_err();
assert_eq!(err.code, ERROR_BAD_CHUNK);
}
#[test]
fn the_first_piece_is_authorised_before_anything_is_buffered() {
let mut up = Uploads::default();
let deny = |_: &str| Err(ServiceError::new(crate::error::ERROR_NOT_ALLOWED, "no"));
let err = up
.accept_with(&piece("u", 0, false, "{\"a\":1", Some("m")), 1000, &deny)
.unwrap_err();
assert_eq!(err.code, crate::error::ERROR_NOT_ALLOWED);
assert_eq!(up.buffered_bytes(), 0);
let err = up
.accept_with(&piece("u", 1, true, "}", None), 1000, &|_| Ok(()))
.unwrap_err();
assert_eq!(err.code, ERROR_BAD_CHUNK);
up.accept(&piece("v", 0, false, "{", Some("m")), 1000)
.unwrap();
assert_eq!(up.buffered_bytes(), 1);
assert!(up
.accept_with(&piece("w", 0, false, "{", Some("m")), 1000, &deny)
.is_err());
assert_eq!(up.buffered_bytes(), 0);
}
#[test]
fn deep_or_dense_bodies_are_refused_before_the_parse() {
let deep = format!(
"{}{}",
"[".repeat(MAX_JSON_DEPTH + 1),
"]".repeat(MAX_JSON_DEPTH + 1)
);
assert_eq!(check_shape(&deep).unwrap_err().code, ERROR_BAD_CHUNK);
let ok = format!(
"{}{}",
"[".repeat(MAX_JSON_DEPTH),
"]".repeat(MAX_JSON_DEPTH)
);
assert!(check_shape(&ok).is_ok());
let dense = format!("[{}0]", "0,".repeat(MAX_JSON_NODES));
assert_eq!(
check_shape(&dense).unwrap_err().code,
ERROR_REQUEST_TOO_LARGE
);
let text = serde_json::to_string(&json!({"s": "[[[[,,,,\"]]".repeat(50_000)})).unwrap();
assert!(check_shape(&text).is_ok());
let mut up = Uploads::default();
let err = up
.accept(&piece("u", 0, true, &deep, Some("m")), 1 << 20)
.unwrap_err();
assert_eq!(err.code, ERROR_BAD_CHUNK);
assert_eq!(up.buffered_bytes(), 0);
}
#[test]
fn an_idle_assembly_expires_and_a_failure_frees_it() {
let mut up = Uploads::default();
up.accept(&piece("u", 0, false, "{", Some("m")), 1000)
.unwrap();
assert_eq!(up.buffered_bytes(), 1);
let Some(stale) = Instant::now().checked_sub(UPLOAD_IDLE_TIMEOUT + Duration::from_secs(1))
else {
return; };
up.current.as_mut().unwrap().touched = stale;
up.expire();
assert_eq!(up.buffered_bytes(), 0);
up.accept(&piece("u", 0, false, "{", Some("m")), 1000)
.unwrap();
assert!(up.accept(&piece("u", 5, false, "x", None), 1000).is_err());
assert_eq!(up.buffered_bytes(), 0);
}
#[test]
fn chunking_is_off_by_default() {
assert_eq!(DEFAULT_MAX_MESSAGE, 0);
}
#[test]
fn a_download_serves_every_piece_and_rejects_strangers() {
let text = "héllo wörld ".repeat(40);
let dl = Download::new("d1".into(), text.clone(), 25);
let desc = dl.descriptor();
assert_eq!(desc["bytes"], text.len());
let count = desc["count"].as_u64().unwrap();
let mut joined = String::new();
for seq in 0..count {
let r = dl.fetch(&json!({"id": "d1", "seq": seq})).unwrap();
joined.push_str(r["text"].as_str().unwrap());
}
assert_eq!(joined, text);
assert!(dl.fetch(&json!({"id": "d1", "seq": count})).is_err());
assert!(dl.fetch(&json!({"id": "d2", "seq": 0})).is_err());
}
#[test]
fn the_chunk_size_scales_with_the_frame_limit() {
assert_eq!(chunk_bytes_for(16 * 1024 * 1024), CHUNK_TEXT_BYTES);
assert_eq!(chunk_bytes_for(4096), 1024);
assert_eq!(chunk_bytes_for(10), 16);
}
}