Files
Marcos Paulo bc0b258e04 feat: engine and core crates: comment sidecars hidden and removed with the note, e2e comments test, extension tables, annotate text and arrow items
Sidecars (*.comments.json) stay out of the index and listings and follow rename and delete of their note; end-to-end comments test over a stub server (Ana comments and mentions Bob, Bob resolves, second sync changes nothing); viewer extension tables gain ico, ogv, 3gp, opus, aac; annotate crate: Edit::Replace for undoable text moves and edits, line items carry an arrow flag; syntax test fixed (TeX is a bundled grammar). Built by MiniMax M3.1 Flash and Claude Sonnet; verified with the full cargo xtask ci on the combined tree (4422 tests, clippy -D warnings, layers, tree, i18n).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 02:31:52 -03:00

102 lines
3.4 KiB
Rust

//! The wire of the fake server: reading a request and writing an answer.
//!
//! The answers speak plain HTTP/1.1: a status line, a `Content-Length` and the
//! body. The bodies of the file routes are raw bytes, so a note travels the
//! fake exactly as it is on disk.
use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use serde_json::Value;
/// Most bytes one request body may hold; the bodies of the tests are tiny.
const MAX_BODY_BYTES: usize = 32 * 1024 * 1024;
/// One request the fake received.
#[derive(Debug, Clone, Default)]
pub struct Request {
/// The HTTP method of the request line.
pub method: String,
/// The path of the request line, with its query.
pub path: String,
/// The `Authorization` header, when there was one.
pub auth: Option<String>,
/// The headers of the request, with their names in lower case.
pub headers: Vec<(String, String)>,
/// The body of the request.
pub body: Vec<u8>,
}
impl Request {
/// The value of a header, looked up by its lower-case name.
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.iter().find(|(key, _)| key == name).map(|(_, value)| value.as_str())
}
/// The body of the request as text.
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
/// The body of the request as JSON; a body that is not JSON is `null`.
pub fn json(&self) -> Value {
serde_json::from_slice(&self.body).unwrap_or(Value::Null)
}
}
/// Reads one request of a connection; `None` when the client closed it.
pub fn read_request(reader: &mut BufReader<TcpStream>) -> Option<Request> {
let mut line = String::new();
reader.read_line(&mut line).ok()?;
if line.trim().is_empty() {
return None;
}
let mut parts = line.split_whitespace();
let method = parts.next()?.to_owned();
let path = parts.next()?.to_owned();
let mut auth = None;
let mut headers: Vec<(String, String)> = Vec::new();
let mut length = 0_usize;
loop {
let mut header = String::new();
if reader.read_line(&mut header).ok()? == 0 {
break;
}
let trimmed = header.trim_end();
if trimmed.is_empty() {
break;
}
let Some((name, value)) = trimmed.split_once(':') else { continue };
let name = name.trim().to_ascii_lowercase();
let value = value.trim().to_owned();
if name == "authorization" {
auth = Some(value.clone());
}
if name == "content-length" {
length = value.parse().unwrap_or(0).min(MAX_BODY_BYTES);
}
headers.push((name, value));
}
let mut body = vec![0_u8; length];
if length > 0 {
reader.read_exact(&mut body).ok()?;
}
Some(Request { method, path, auth, headers, body })
}
/// Writes one answer of the fake.
pub fn write_answer(stream: &mut TcpStream, status: u16, content_type: &str, body: &[u8]) {
let reason = match status {
200 => "OK",
201 => "Created",
400 => "Bad Request",
404 => "Not Found",
409 => "Conflict",
_ => "Status",
};
let head = format!("HTTP/1.1 {status} {reason}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\n\r\n", body.len());
let _ = stream.write_all(head.as_bytes());
let _ = stream.write_all(body);
let _ = stream.flush();
}