use super::headers::HeaderMap;
use super::request::Version;
use super::status::HttpStatusCode;
use bytes::Bytes;
use std::fs::File;
use std::path::PathBuf;
use std::sync::Arc;
/// Server token shown in the default error-page footer and `Server` header,
/// e.g. `veld/0.1.0` (analogous to nginx's `nginx/1.20.1`).
const SERVER_TOKEN: &str = concat!("veld/", env!("CARGO_PKG_VERSION"));
/// Self-contained (no external assets / JS) styled default error page.
/// Placeholders `__CODE__`, `__REASON__`, `__SERVER__` are substituted at
/// render time.
const ERROR_PAGE_TEMPLATE: &str = r##"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>__CODE__ __REASON__</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{display:flex;align-items:center;justify-content:center;overflow:hidden;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;
color:#e8e8ea;
background:radial-gradient(1200px 620px at 50% -12%,#2a1206 0%,#0b0b0f 56%,#06060a 100%)}
.grid{position:fixed;inset:0;z-index:0;opacity:.55;
background-image:linear-gradient(rgba(255,255,255,.035) 1px,transparent 1px),
linear-gradient(90deg,rgba(255,255,255,.035) 1px,transparent 1px);
background-size:44px 44px;
-webkit-mask-image:radial-gradient(circle at 50% 34%,#000,transparent 72%);
mask-image:radial-gradient(circle at 50% 34%,#000,transparent 72%)}
.wrap{position:relative;z-index:1;text-align:center;padding:2rem}
.code{font-family:"SFMono-Regular",ui-monospace,Menlo,Consolas,monospace;
font-weight:800;font-size:clamp(6rem,23vw,14rem);line-height:.92;letter-spacing:-.045em;
background:linear-gradient(180deg,#ff8a4c 0%,#f74c00 48%,#b3320a 100%);
-webkit-background-clip:text;background-clip:text;color:transparent;
animation:glow 3.4s ease-in-out infinite}
@keyframes glow{0%,100%{filter:drop-shadow(0 0 26px rgba(247,76,0,.35))}
50%{filter:drop-shadow(0 0 54px rgba(247,76,0,.7))}}
.reason{margin-top:.5rem;font-size:clamp(1.15rem,3.6vw,1.9rem);font-weight:700;color:#f4f4f6}
.desc{margin-top:.95rem;font-size:.96rem;color:#9a9aa3;line-height:1.6}
.actions{margin-top:1.7rem}
.btn{display:inline-flex;align-items:center;gap:.55rem;padding:.72rem 1.4rem;border-radius:999px;
font-size:.96rem;font-weight:700;text-decoration:none;color:#1a0a02;
background:linear-gradient(180deg,#ff8a4c,#f74c00);
box-shadow:0 10px 30px rgba(247,76,0,.42);transition:transform .16s ease,box-shadow .16s ease}
.btn:hover{transform:translateY(-2px);box-shadow:0 14px 40px rgba(247,76,0,.6)}
.btn .arrow{transition:transform .16s ease}
.btn:hover .arrow{transform:translateX(4px)}
.bar{margin:2rem auto 1.1rem;width:min(420px,72vw);height:1px;
background:linear-gradient(90deg,transparent,rgba(247,76,0,.65),transparent)}
.brand{display:inline-flex;align-items:center;gap:.55rem;
font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.84rem;color:#c9c9cf;letter-spacing:.06em}
.brand .dot{width:8px;height:8px;border-radius:50%;background:#f74c00;
box-shadow:0 0 10px #f74c00;animation:blink 1.6s steps(2,start) infinite}
@keyframes blink{50%{opacity:.25}}
</style>
</head>
<body>
<div class="grid"></div>
<div class="wrap">
<div class="code">__CODE__</div>
<div class="reason">__REASON__</div>
<div class="desc">抱歉,请求的页面无法访问。</div>
<div class="actions"><a class="btn" href="https://netditec.com">前往官网 netditec.com <span class="arrow">→</span></a></div>
<div class="bar"></div>
<div class="brand"><span class="dot"></span>__SERVER__</div>
</div>
</body>
</html>
"##;
/// Response body
#[derive(Debug, Clone)]
pub enum Body {
Empty,
Bytes(Bytes),
File(PathBuf, u64), // path, size
/// An already-open, read-only file descriptor plus its size. Allows the
/// connection layer to `sendfile(2)` directly from a cached descriptor
/// without re-opening the file.
FileFd(Arc<File>, u64),
/// A fully precomputed cached response. The status line, headers, and
/// (for small files) the body are serialized once at cache-fill time so
/// the hot path performs no allocation or formatting -- just a single
/// `write` (small files) or a header `write` plus `sendfile` (large
/// files). The boolean marks a HEAD request (headers only).
Cached(Arc<CachedResponse>, bool),
}
/// A precomputed, ready-to-send response for a cached static file.
#[derive(Debug)]
pub struct CachedResponse {
/// Full response bytes (status line + headers + body) for files small
/// enough to keep resident; sent in a single `write`. `None` for large
/// files, which are streamed via `sendfile`.
pub full: Option<Bytes>,
/// Status line + headers + terminating CRLF (no body). Used for the
/// `sendfile` path and for HEAD requests.
pub header_block: Bytes,
/// Open, read-only descriptor for the `sendfile` path.
pub file: Arc<File>,
/// File size in bytes.
pub size: u64,
}
impl Body {
pub fn len(&self) -> usize {
match self {
Body::Empty => 0,
Body::Bytes(b) => b.len(),
Body::File(_, size) => *size as usize,
Body::FileFd(_, size) => *size as usize,
Body::Cached(c, _) => c.size as usize,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Get body as bytes (for non-file bodies)
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Body::Bytes(b) => Some(b),
_ => None,
}
}
/// Convert into inner bytes if this is a Bytes variant.
pub fn into_bytes(self) -> Option<Bytes> {
match self {
Body::Bytes(b) => Some(b),
_ => None,
}
}
}
impl From<Vec<u8>> for Body {
fn from(v: Vec<u8>) -> Self {
Body::Bytes(Bytes::from(v))
}
}
impl From<Bytes> for Body {
fn from(b: Bytes) -> Self {
Body::Bytes(b)
}
}
impl From<String> for Body {
fn from(s: String) -> Self {
Body::Bytes(Bytes::from(s))
}
}
/// HTTP response
#[derive(Debug, Clone)]
pub struct Response {
pub status: HttpStatusCode,
pub version: Version,
pub headers: HeaderMap,
pub body: Body,
pub close_connection: bool,
}
impl Response {
/// Create a new response with default 200 OK status.
pub fn new() -> Self {
Self {
status: HttpStatusCode::OK,
version: Version::Http11,
headers: HeaderMap::new(),
body: Body::Empty,
close_connection: false,
}
}
/// Builder-style: set the status code.
pub fn status(mut self, status: HttpStatusCode) -> Self {
self.status = status;
self
}
/// Builder-style: set a header.
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers.insert(name, value);
self
}
/// Builder-style: set the body from bytes.
pub fn with_body_bytes(mut self, data: Vec<u8>) -> Self {
let len = data.len();
if !self.headers.contains("content-length") {
self.headers.insert("Content-Length", len.to_string());
}
self.body = Body::Bytes(Bytes::from(data));
self
}
/// Builder-style: set the body from a Body enum or Vec<u8>.
pub fn body(mut self, body: impl Into<Body>) -> Self {
self.body = body.into();
self
}
/// Builder-style: set the body from a string.
pub fn body_str(self, s: &str) -> Self {
self.with_body_bytes(s.as_bytes().to_vec())
}
/// Builder-style: set close_connection.
pub fn close(mut self, close: bool) -> Self {
self.close_connection = close;
self
}
/// Get a reference to the body bytes, if present.
pub fn body_bytes(&self) -> Option<&[u8]> {
self.body.as_bytes()
}
/// Get a reference to the body bytes, if present (alias).
pub fn body_bytes_ref(&self) -> Option<&[u8]> {
self.body.as_bytes()
}
pub fn builder() -> ResponseBuilder {
ResponseBuilder::new()
}
pub fn ok() -> Self {
Response::new()
}
/// Build an nginx-style default error page branded for veld.
///
/// Mirrors nginx's default body (`<center>` heading + `<hr>` + a server
/// token footer) so the look is familiar, and sets the `Server` header.
pub fn error_page(status: HttpStatusCode) -> Self {
let code = status.as_u16();
let body = ERROR_PAGE_TEMPLATE
.replace("__CODE__", &code.to_string())
.replace("__REASON__", status.reason_phrase())
.replace("__SERVER__", SERVER_TOKEN);
Response::new()
.status(status)
.header("Server", "veld")
.header("Content-Type", "text/html; charset=utf-8")
.with_body_bytes(body.into_bytes())
}
pub fn not_found() -> Self {
Self::error_page(HttpStatusCode::NOT_FOUND)
}
pub fn bad_request() -> Self {
Self::error_page(HttpStatusCode::BAD_REQUEST)
}
pub fn internal_error() -> Self {
Self::error_page(HttpStatusCode::INTERNAL_SERVER_ERROR)
}
pub fn not_modified() -> Self {
Response::new().status(HttpStatusCode::NOT_MODIFIED)
}
pub fn redirect(url: &str) -> Self {
Response::new()
.status(HttpStatusCode::MOVED_PERMANENTLY)
.header("Location", url)
}
pub fn gateway_timeout() -> Self {
Self::error_page(HttpStatusCode::GATEWAY_TIMEOUT)
}
pub fn bad_gateway() -> Self {
Self::error_page(HttpStatusCode::BAD_GATEWAY)
}
/// Serialize the response to bytes (status line + headers + body).
///
/// Pre-calculates the total size and performs a single allocation,
/// writing the status line, headers, and body directly into the
/// buffer without intermediate allocations.
pub fn to_bytes(&self) -> Vec<u8> {
let reason = self.status.reason_phrase();
// Status line: version + ' ' + status_code(3 digits) + ' ' + reason + \r\n
let status_line_len = self.version.as_str().len() + 1 + 3 + 1 + reason.len() + 2;
let header_block_len = self.headers.serialized_len();
let body_len = self.body.len();
// +2 for the blank line between headers and body
let total = status_line_len + header_block_len + 2 + body_len;
let mut buf = Vec::with_capacity(total);
// Status line -- write status code as ASCII digits directly to
// avoid the temporary String allocation from u16::to_string().
buf.extend_from_slice(self.version.as_str().as_bytes());
buf.push(b' ');
push_status_code(&mut buf, self.status.as_u16());
buf.push(b' ');
buf.extend_from_slice(reason.as_bytes());
buf.extend_from_slice(b"\r\n");
// Headers -- write directly into our buffer instead of creating
// an intermediate Vec via headers.to_bytes().
self.headers.write_to(&mut buf);
// End of headers
buf.extend_from_slice(b"\r\n");
// Body (only for Bytes variant)
if let Body::Bytes(ref b) = self.body {
buf.extend_from_slice(b);
}
buf
}
/// Serialize only the status line and headers (no body).
///
/// Useful for sendfile-style responses where the headers are sent as
/// one write and the body is sent directly from disk via the kernel,
/// avoiding a userspace copy of the file contents.
pub fn to_header_bytes(&self) -> Vec<u8> {
let reason = self.status.reason_phrase();
let status_line_len = self.version.as_str().len() + 1 + 3 + 1 + reason.len() + 2;
let header_block_len = self.headers.serialized_len();
// +2 for the blank line terminating the header block
let total = status_line_len + header_block_len + 2;
let mut buf = Vec::with_capacity(total);
// Status line
buf.extend_from_slice(self.version.as_str().as_bytes());
buf.push(b' ');
push_status_code(&mut buf, self.status.as_u16());
buf.push(b' ');
buf.extend_from_slice(reason.as_bytes());
buf.extend_from_slice(b"\r\n");
// Headers
self.headers.write_to(&mut buf);
// End of headers
buf.extend_from_slice(b"\r\n");
buf
}
/// Set a header (mutating version)
pub fn set_header(&mut self, name: &str, value: &str) {
self.headers.insert(name, value);
}
}
impl Default for Response {
fn default() -> Self {
Self::new()
}
}
/// Response builder (alternative API)
pub struct ResponseBuilder {
status: HttpStatusCode,
version: Version,
headers: HeaderMap,
close_connection: bool,
}
impl ResponseBuilder {
pub fn new() -> Self {
Self {
status: HttpStatusCode::OK,
version: Version::Http11,
headers: HeaderMap::new(),
close_connection: false,
}
}
pub fn status(mut self, status: HttpStatusCode) -> Self {
self.status = status;
self
}
pub fn version(mut self, version: Version) -> Self {
self.version = version;
self
}
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers.insert(name, value);
self
}
pub fn close(mut self, close: bool) -> Self {
self.close_connection = close;
self
}
pub fn build(self) -> Response {
Response {
status: self.status,
version: self.version,
headers: self.headers,
body: Body::Empty,
close_connection: self.close_connection,
}
}
pub fn with_body_bytes(self, data: Vec<u8>) -> Response {
let len = data.len();
let mut headers = self.headers;
if !headers.contains("content-length") {
headers.insert("Content-Length", len.to_string());
}
Response {
status: self.status,
version: self.version,
headers,
body: Body::Bytes(Bytes::from(data)),
close_connection: self.close_connection,
}
}
pub fn body_str(self, s: &str) -> Response {
self.with_body_bytes(s.as_bytes().to_vec())
}
}
impl Default for ResponseBuilder {
fn default() -> Self {
Self::new()
}
}
/// Write a 3-digit HTTP status code directly into the buffer as ASCII bytes.
///
/// This avoids the heap allocation that `u16::to_string()` would create.
/// HTTP status codes are always in the range 100-599, so exactly 3 digits.
#[inline]
fn push_status_code(buf: &mut Vec<u8>, code: u16) {
// Safe: code is always 100..=599, producing valid ASCII digit bytes.
buf.push(b'0' + (code / 100) as u8);
buf.push(b'0' + ((code / 10) % 10) as u8);
buf.push(b'0' + (code % 10) as u8);
}