use serde::Serialize;
use std::io::{self, Write};
use crate::profusegw_response::{ProfuseGwCode, ProfuseGwResponse};
const PREFIX: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ";
const SUFFIX: &[u8] = b"\r\nConnection: close\r\n\r\n";
const HEADER_RESERVE: usize = PREFIX.len() + 20 + SUFFIX.len();
const BODY_LIMIT: usize = saddle_boundary::ingress::MAX_BODY_BYTES;
pub(crate) const RESPONSE_BUFFER_BYTES: usize = BODY_LIMIT + HEADER_RESERVE;
struct BoundedResponse(Vec<u8>);
impl Write for BoundedResponse {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
if bytes.len() > RESPONSE_BUFFER_BYTES - self.0.len() {
return Err(io::Error::other("response payload limit exceeded"));
}
self.0.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[doc(hidden)]
pub fn encode_profusegw_http1<Data, Code>(
response: &ProfuseGwResponse<Data, Code>,
) -> Result<Vec<u8>, serde_json::Error>
where
Data: Serialize,
Code: ProfuseGwCode,
{
let mut output = BoundedResponse(Vec::with_capacity(RESPONSE_BUFFER_BYTES));
output.0.resize(HEADER_RESERVE, 0);
serde_json::to_writer(&mut output, response)?;
let body_len = output.0.len() - HEADER_RESERVE;
let mut header = [0_u8; HEADER_RESERVE];
let mut cursor = io::Cursor::new(header.as_mut_slice());
cursor.write_all(PREFIX).expect("fixed header fits");
write!(cursor, "{body_len}").expect("usize fits reserved decimal width");
cursor.write_all(SUFFIX).expect("fixed header fits");
let header_len = cursor.position() as usize;
output.0.copy_within(HEADER_RESERVE.., header_len);
output.0[..header_len].copy_from_slice(&header[..header_len]);
output.0.truncate(header_len + body_len);
Ok(output.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profusegw_response::{
FailureMessage, ProfuseGwCode, ProfuseGwFailure, ProfuseGwResponse,
};
#[derive(Serialize)]
struct Data {
count: u64,
}
enum Code {
Rejected,
}
impl ProfuseGwCode for Code {
const REGISTERED_CODES: &'static [&'static str] = &["ACCOUNT_REJECTED"];
fn stable_code(&self) -> &'static str {
"ACCOUNT_REJECTED"
}
}
#[test]
fn response_limit_checks_encoded_bytes_and_recovers() {
let overhead = br#"{"success":true,"data":""}"#.len();
for size in [BODY_LIMIT - 1, BODY_LIMIT] {
let value = "a".repeat(size - overhead);
let response = ProfuseGwResponse::<_, Code>::success(value);
let wire = encode_profusegw_http1(&response).unwrap();
let split = wire.windows(4).position(|w| w == b"\r\n\r\n").unwrap() + 4;
assert_eq!(wire.len() - split, size);
assert_eq!(wire.capacity(), RESPONSE_BUFFER_BYTES);
}
for value in [
"a".repeat(BODY_LIMIT + 1 - overhead),
"\0".repeat(BODY_LIMIT / 6),
] {
assert!(encode_profusegw_http1(&ProfuseGwResponse::<_, Code>::success(value)).is_err());
}
assert!(
encode_profusegw_http1(&ProfuseGwResponse::<_, Code>::success("recovered")).is_ok()
);
}
#[test]
fn rejected_append_does_not_grow_or_partially_write() {
let mut writer = BoundedResponse(Vec::with_capacity(RESPONSE_BUFFER_BYTES));
writer.0.resize(RESPONSE_BUFFER_BYTES - 1, 0);
let pointer = writer.0.as_ptr();
assert!(writer.write_all(b"xx").is_err());
assert_eq!(writer.0.len(), RESPONSE_BUFFER_BYTES - 1);
assert_eq!(writer.0.as_ptr(), pointer);
writer.write_all(b"x").unwrap();
assert_eq!(writer.0.as_ptr(), pointer);
assert!(writer.write_all(b"x").is_err());
}
#[test]
fn legal_branches_share_exact_json_http_200_projection() {
let success = ProfuseGwResponse::<_, Code>::success(Data { count: 7 });
assert_eq!(
encode_profusegw_http1(&success).unwrap(),
b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 35\r\nConnection: close\r\n\r\n{\"success\":true,\"data\":{\"count\":7}}"
);
let failure = ProfuseGwResponse::<Data, _>::failure(ProfuseGwFailure::new(
Code::Rejected,
FailureMessage::new("Account was rejected").unwrap(),
));
assert_eq!(
encode_profusegw_http1(&failure).unwrap(),
b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 88\r\nConnection: close\r\n\r\n{\"success\":false,\"failure\":{\"code\":\"ACCOUNT_REJECTED\",\"message\":\"Account was rejected\"}}"
);
}
}