bc0b258e04
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>
102 lines
3.4 KiB
Rust
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();
|
|
}
|