QZ4ZAYJJGG3NWG4KB5ZDDP6JAQX2UYDQX7PRKVIMPSAOBC6U543AC //! HTTP responses//!//! # Content Types//! - `application/octet-stream`: Binary data (changes, tags, RemoteId)//! - `text/plain`: Text responses (changelist, state)//! - `application/json`: Structured data (identities, channel info)//!//! # Caching//! Change and tag files are content-addressed and immutable, so they're//! served caching headers (1 year, immutable).use std::io::Cursor;use std::path::Path;use tiny_http::{Header, Response, StatusCode};pub type HttpResponse = Response<Cursor<Vec<u8>>>;/// Create a 200 OK response with application/octet-stream.pub fn ok_bin(data: Vec<u8>) -> HttpResponse {Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/octet-stream").unwrap())}/// Create a 200 OK response with text/plain.pub fn ok_text(s: String) -> HttpResponse {Response::from_data(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "text/plain").unwrap())}/// Create a 200 OK response with application/json.pub fn ok_json(s: &str) -> HttpResponse {Response::from_string(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/json").unwrap())}/// Create a 400 Bad Request response.pub fn bad_request() -> HttpResponse {Response::from_string("bad request\n").with_status_code(StatusCode(400))}/// Create a 404 Not Found response with a message.////// Returned when:/// - Requested change/tag file doesn't exist/// - Channel doesn't exist/// - Path doesn't start with `.pijul`/// - Hash validation failspub fn not_found(msg: &str) -> HttpResponse {Response::from_string(format!("{msg}\n")).with_status_code(StatusCode(404))}/// Create a 500 Internal Server Error response.pub fn internal_error() -> HttpResponse {Response::from_string("internal error\n").with_status_code(StatusCode(500))}/// Serve a file from disk with caching headers.////// Used by `serve_change()` and `serve_tag()` to serve binary change/tag files.////// Returns 404 if the file doesn't exist or can't be read.pub fn serve_file(path: &Path, content_type: &str) -> HttpResponse {match std::fs::read(path) {Ok(data) => Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", content_type).unwrap()).with_header(Header::from_bytes("Cache-Control", "public, max-age=31536000, immutable").unwrap(),),Err(_) => not_found("not found"),}}#[cfg(test)]mod tests {use super::*;fn get_status(resp: &HttpResponse) -> u16 {resp.status_code().0}fn get_header<'a>(resp: &'a HttpResponse, name: &str) -> Option<&'a str> {resp.headers().iter().find(|h| h.field.as_str().as_str().eq_ignore_ascii_case(name)).map(|h| h.value.as_str())}#[test]fn test_ok_bin() {let data = vec![0x01, 0x02, 0x03];let resp = ok_bin(data);assert_eq!(get_status(&resp), 200);assert_eq!(get_header(&resp, "Content-Type"),Some("application/octet-stream"));}#[test]fn test_ok_text() {let resp = ok_text("hello world".to_string());assert_eq!(get_status(&resp), 200);assert_eq!(get_header(&resp, "Content-Type"), Some("text/plain"));}#[test]fn test_ok_json() {let resp = ok_json(r#"{"key": "value"}"#);assert_eq!(get_status(&resp), 200);assert_eq!(get_header(&resp, "Content-Type"), Some("application/json"));}#[test]fn test_bad_request() {let resp = bad_request();assert_eq!(get_status(&resp), 400);}#[test]fn test_not_found() {let resp = not_found("resource missing");assert_eq!(get_status(&resp), 404);}#[test]fn test_internal_error() {let resp = internal_error();assert_eq!(get_status(&resp), 500);}#[test]fn test_serve_file_not_found() {let resp = serve_file(Path::new("/nonexistent/path/file.txt"), "text/plain");assert_eq!(get_status(&resp), 404);}#[test]fn test_serve_file_exists() {use std::io::Write;let mut file = tempfile::NamedTempFile::new().unwrap();writeln!(file, "File is served!").unwrap();let resp = serve_file(&file.path(), "text/plain");assert_eq!(get_status(&resp), 200);assert_eq!(get_header(&resp, "Content-Type"), Some("text/plain"));assert_eq!(get_header(&resp, "Cache-Control"),Some("public, max-age=31536000, immutable"));}}
/* ---------------- helpers ---------------- */fn serve_file(path: &Path, ty: &str) -> Response<std::io::Cursor<Vec<u8>>> {match std::fs::read(path) {Ok(data) => Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", ty).unwrap()).with_header(Header::from_bytes("Cache-Control", "public, max-age=31536000, immutable").unwrap(),),Err(_) => not_found("not found"),}}/* ---------------- responses ---------------- */fn ok_bin(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/octet-stream").unwrap())}fn ok_text(s: String) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_data(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "text/plain").unwrap())}fn ok_json(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/json").unwrap())}fn bad_request() -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string("bad request\n").with_status_code(StatusCode(400))}fn not_found(msg: &str) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string(format!("{msg}\n")).with_status_code(StatusCode(404))}fn internal_error() -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string("internal error\n").with_status_code(StatusCode(500))}
[[package]]name = "equivalent"version = "1.0.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"[[package]]name = "errno"version = "0.3.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"dependencies = ["libc","windows-sys",]
name = "foldhash"version = "0.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"[[package]]name = "getrandom"version = "0.4.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"dependencies = ["cfg-if","libc","r-efi","wasip2","wasip3",][[package]]
name = "hashbrown"version = "0.15.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"dependencies = ["foldhash",][[package]]name = "hashbrown"version = "0.16.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"[[package]]name = "heck"version = "0.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"[[package]]
][[package]]name = "wasip2"version = "1.0.2+wasi-0.2.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"dependencies = ["wit-bindgen",][[package]]name = "wasip3"version = "0.4.0+wasi-0.3.0-rc-2026-01-06"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"dependencies = ["wit-bindgen",
name = "wasm-encoder"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"dependencies = ["leb128fmt","wasmparser",][[package]]name = "wasm-metadata"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"dependencies = ["anyhow","indexmap","wasm-encoder","wasmparser",][[package]]name = "wasmparser"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"dependencies = ["bitflags","hashbrown 0.15.5","indexmap","semver",][[package]]
name = "wit-bindgen-core"version = "0.51.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"dependencies = ["anyhow","heck","wit-parser",][[package]]name = "wit-bindgen-rust"version = "0.51.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"dependencies = ["anyhow","heck","indexmap","prettyplease","syn","wasm-metadata","wit-bindgen-core","wit-component",][[package]]name = "wit-bindgen-rust-macro"version = "0.51.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"dependencies = ["anyhow","prettyplease","proc-macro2","quote","syn","wit-bindgen-core","wit-bindgen-rust",][[package]]name = "wit-component"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"dependencies = ["anyhow","bitflags","indexmap","log","serde","serde_derive","serde_json","wasm-encoder","wasm-metadata","wasmparser","wit-parser",][[package]]name = "wit-parser"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"dependencies = ["anyhow","id-arena","indexmap","log","semver","serde","serde_derive","serde_json","unicode-xid","wasmparser",][[package]]