saddle-framework 0.3.12

The single business-facing facade for Saddle applications
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(())
    }
}

/// Production-only projection from the framework's sole closed Profusegw
/// response into its fixed HTTP/1.1 carrier.
///
/// Legal Success and Failure values share HTTP 200. A response that cannot be
/// serialized is returned to the listener as a protocol error and is never
/// framed as a successful response.
///
/// A foreign serializable response cannot acquire this carrier:
///
/// ```compile_fail
/// use serde::Serialize;
/// #[derive(Serialize)]
/// struct ForeignResponse { success: bool }
/// let foreign = ForeignResponse { success: true };
/// let _ = saddle::__private::encode_profusegw_http1(&foreign);
/// ```
#[doc(hidden)]
pub fn encode_profusegw_http1<Data, Code>(
    response: &ProfuseGwResponse<Data, Code>,
) -> Result<Vec<u8>, serde_json::Error>
where
    Data: Serialize,
    Code: ProfuseGwCode,
{
    // One allocation, including framing. Escaped JSON bytes are checked before
    // every append; neither serialization nor framing can grow this allocation.
    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\"}}"
        );
    }
}