ove/
net_http.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! HTTP/1.1 client with RAII response management.
8//!
9//! [`Client`] wraps the oveRTOS HTTP client handle with automatic resource
10//! cleanup.  [`Response`] owns the heap-allocated body and headers returned
11//! by `ove_http_get` / `ove_http_post` and frees them on drop.
12//!
13//! Works in both heap and zero-heap modes.
14
15use core::fmt;
16
17use crate::bindings;
18use crate::error::{Error, Result};
19
20// ---------------------------------------------------------------------------
21// Method
22// ---------------------------------------------------------------------------
23
24/// HTTP request method.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Method {
27    /// HTTP GET.
28    Get,
29    /// HTTP POST.
30    Post,
31    /// HTTP PUT.
32    Put,
33    /// HTTP DELETE.
34    Delete,
35    /// HTTP PATCH.
36    Patch,
37}
38
39impl Method {
40    fn to_c(self) -> bindings::ove_http_method_t {
41        match self {
42            Method::Get => bindings::OVE_HTTP_GET,
43            Method::Post => bindings::OVE_HTTP_POST,
44            Method::Put => bindings::OVE_HTTP_PUT,
45            Method::Delete => bindings::OVE_HTTP_DELETE,
46            Method::Patch => bindings::OVE_HTTP_PATCH,
47        }
48    }
49}
50
51// ---------------------------------------------------------------------------
52// Header
53// ---------------------------------------------------------------------------
54
55/// HTTP request header.
56///
57/// Both `name` and `value` must be null-terminated byte strings
58/// (e.g. `b"Authorization\0"`, `b"Bearer token\0"`).
59pub struct Header<'a> {
60    pub name: &'a [u8],
61    pub value: &'a [u8],
62}
63
64// ---------------------------------------------------------------------------
65// ClientStorage
66// ---------------------------------------------------------------------------
67
68/// Backing storage for an HTTP client in zero-heap mode.
69///
70/// In heap mode this is a zero-size placeholder.  Pass a `&mut ClientStorage`
71/// to [`Client::create`] — the borrow checker ensures the storage outlives
72/// the client.
73///
74/// ```ignore
75/// let mut storage = ClientStorage::new();
76/// let client = Client::create(&mut storage)?;
77/// ```
78pub struct ClientStorage(bindings::ove_http_client_storage_t);
79
80impl ClientStorage {
81    pub fn new() -> Self {
82        Self(unsafe { core::mem::zeroed() })
83    }
84}
85
86// ---------------------------------------------------------------------------
87// Response
88// ---------------------------------------------------------------------------
89
90/// HTTP response with RAII lifetime for body and header buffers.
91///
92/// Returned by [`Client::get`] and [`Client::post`].  The body and header
93/// memory is freed automatically when this value is dropped.
94pub struct Response {
95    raw: bindings::ove_http_response_t,
96}
97
98impl Response {
99    /// HTTP status code (e.g. 200, 404).
100    pub fn status(&self) -> i32 {
101        self.raw.status
102    }
103
104    /// Response body as a byte slice (empty if no body).
105    pub fn body(&self) -> &[u8] {
106        if self.raw.body.is_null() || self.raw.body_len == 0 {
107            return &[];
108        }
109        unsafe { core::slice::from_raw_parts(self.raw.body as *const u8, self.raw.body_len) }
110    }
111
112    /// Raw response headers as a byte slice (empty if none).
113    pub fn headers(&self) -> &[u8] {
114        if self.raw.headers.is_null() || self.raw.headers_len == 0 {
115            return &[];
116        }
117        unsafe {
118            core::slice::from_raw_parts(self.raw.headers as *const u8, self.raw.headers_len)
119        }
120    }
121}
122
123impl fmt::Debug for Response {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        f.debug_struct("Response")
126            .field("status", &self.raw.status)
127            .field("body_len", &self.raw.body_len)
128            .field("headers_len", &self.raw.headers_len)
129            .finish()
130    }
131}
132
133impl Drop for Response {
134    fn drop(&mut self) {
135        unsafe { bindings::ove_http_response_free(&mut self.raw) }
136    }
137}
138
139// ---------------------------------------------------------------------------
140// Client
141// ---------------------------------------------------------------------------
142
143/// HTTP/1.1 client.
144///
145/// Wraps `ove_http_client_t` with automatic cleanup on drop.
146pub struct Client {
147    handle: bindings::ove_http_client_t,
148}
149
150impl Client {
151    /// Create a new HTTP client via heap allocation (only in heap mode).
152    #[cfg(not(zero_heap))]
153    pub fn new() -> Result<Self> {
154        let mut handle: bindings::ove_http_client_t = core::ptr::null_mut();
155        let rc = unsafe { bindings::ove_http_client_create(&mut handle) };
156        Error::from_code(rc)?;
157        Ok(Self { handle })
158    }
159
160    /// Create from caller-provided static storage.
161    ///
162    /// # Safety
163    /// Caller must ensure `storage` outlives the `Client` and is not
164    /// shared with another primitive.
165    #[cfg(zero_heap)]
166    pub unsafe fn from_static(
167        storage: *mut bindings::ove_http_client_storage_t,
168    ) -> Result<Self> {
169        let mut handle: bindings::ove_http_client_t = core::ptr::null_mut();
170        let rc = unsafe { bindings::ove_http_client_init(&mut handle, storage) };
171        Error::from_code(rc)?;
172        Ok(Self { handle })
173    }
174
175    /// Create a client that works in both heap and zero-heap modes.
176    ///
177    /// In heap mode, allocates via the RTOS heap.  In zero-heap mode,
178    /// uses caller-provided `storage`.  The borrow checker ensures the
179    /// storage outlives the client.
180    pub fn create(storage: &mut ClientStorage) -> Result<Self> {
181        #[cfg(not(zero_heap))]
182        {
183            let _ = storage;
184            Self::new()
185        }
186        #[cfg(zero_heap)]
187        {
188            unsafe { Self::from_static(&mut storage.0) }
189        }
190    }
191
192    /// Perform an HTTP GET request.
193    ///
194    /// `url` must be a null-terminated URL (e.g. `b"http://example.com/path\0"`).
195    ///
196    /// # Errors
197    /// Returns an error if the request fails at the transport or protocol level.
198    pub fn get(&self, url: &[u8]) -> Result<Response> {
199        let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
200        let rc = unsafe {
201            bindings::ove_http_get(self.handle, url.as_ptr() as *const _, &mut raw)
202        };
203        Error::from_code(rc)?;
204        Ok(Response { raw })
205    }
206
207    /// Perform an HTTP POST request.
208    ///
209    /// `url` and `content_type` must be null-terminated byte strings.
210    ///
211    /// # Errors
212    /// Returns an error if the request fails at the transport or protocol level.
213    pub fn post(
214        &self,
215        url: &[u8],
216        content_type: &[u8],
217        body: &[u8],
218    ) -> Result<Response> {
219        let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
220        let rc = unsafe {
221            bindings::ove_http_post(
222                self.handle,
223                url.as_ptr() as *const _,
224                content_type.as_ptr() as *const _,
225                body.as_ptr() as *const _,
226                body.len(),
227                &mut raw,
228            )
229        };
230        Error::from_code(rc)?;
231        Ok(Response { raw })
232    }
233
234    /// Perform an HTTP request with explicit method, optional body, and
235    /// optional extra headers.
236    ///
237    /// `url` and `content_type` must be null-terminated byte strings.
238    /// Each [`Header`] name/value must also be null-terminated.
239    ///
240    /// # Errors
241    /// Returns an error if the request fails at the transport or protocol level.
242    pub fn request_ex(
243        &self,
244        method: Method,
245        url: &[u8],
246        content_type: &[u8],
247        body: &[u8],
248        headers: &[Header<'_>],
249    ) -> Result<Response> {
250        // Convert Header slices into the C struct array on the stack.
251        const MAX_HEADERS: usize = 16;
252        let count = headers.len().min(MAX_HEADERS);
253        let mut c_headers: [bindings::ove_http_header_t; MAX_HEADERS] =
254            unsafe { core::mem::zeroed() };
255        for i in 0..count {
256            c_headers[i].name = headers[i].name.as_ptr() as *const _;
257            c_headers[i].value = headers[i].value.as_ptr() as *const _;
258        }
259
260        let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
261        let rc = unsafe {
262            bindings::ove_http_request_ex(
263                self.handle,
264                method.to_c(),
265                url.as_ptr() as *const _,
266                content_type.as_ptr() as *const _,
267                body.as_ptr() as *const _,
268                body.len(),
269                c_headers.as_ptr(),
270                count,
271                &mut raw,
272            )
273        };
274        Error::from_code(rc)?;
275        Ok(Response { raw })
276    }
277}
278
279impl fmt::Debug for Client {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        f.debug_struct("Client")
282            .field("handle", &format_args!("{:p}", self.handle))
283            .finish()
284    }
285}
286
287impl Drop for Client {
288    fn drop(&mut self) {
289        if self.handle.is_null() { return; }
290        #[cfg(not(zero_heap))]
291        unsafe { bindings::ove_http_client_destroy(self.handle) }
292        #[cfg(zero_heap)]
293        unsafe { bindings::ove_http_client_deinit(self.handle) }
294    }
295}
296
297// SAFETY: Wraps a ove handle. GET/POST are thread-safe RTOS calls.
298// Create/destroy are single-threaded (lifecycle guarantee).
299unsafe impl Send for Client {}
300unsafe impl Sync for Client {}