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//! Blocking 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//!
15//! ## Async alternative
16//!
17//! For async HTTP on top of [`crate::async_net`] use the
18//! [`reqwless`](https://crates.io/crates/reqwless) crate from crates.io.
19//! Supports HTTPS via `embedded-tls`, chunked encoding, redirects, and
20//! produces an `embedded_io_async::Read` body. See [`crate::async_net`]'s
21//! module docs for the full pairing recipe.
22
23use core::fmt;
24
25use crate::bindings;
26use crate::error::{Error, Result};
27
28// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
29// calls below): any handle passed to the C API is non-null and refers to a
30// live RTOS object — wrapper constructors establish validity via
31// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
32// a handle is released. Pointer and slice arguments reference caller-owned
33// memory valid for the duration of the call; the C side copies whatever it
34// retains and does not alias them past return (verified against the
35// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
36// pointer casts from user data, slice reconstruction via `from_raw_parts`,
37// or storing a callback across the FFI boundary — carry their own
38// `// SAFETY:` comment.
39
40// ---------------------------------------------------------------------------
41// Method
42// ---------------------------------------------------------------------------
43
44/// HTTP request method.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Method {
47 /// HTTP GET.
48 Get,
49 /// HTTP POST.
50 Post,
51 /// HTTP PUT.
52 Put,
53 /// HTTP DELETE.
54 Delete,
55 /// HTTP PATCH.
56 Patch,
57}
58
59impl Method {
60 fn to_c(self) -> bindings::ove_http_method_t {
61 match self {
62 Method::Get => bindings::OVE_HTTP_GET,
63 Method::Post => bindings::OVE_HTTP_POST,
64 Method::Put => bindings::OVE_HTTP_PUT,
65 Method::Delete => bindings::OVE_HTTP_DELETE,
66 Method::Patch => bindings::OVE_HTTP_PATCH,
67 }
68 }
69}
70
71// ---------------------------------------------------------------------------
72// Header
73// ---------------------------------------------------------------------------
74
75/// HTTP request header.
76///
77/// Both `name` and `value` must be null-terminated byte strings
78/// (e.g. `b"Authorization\0"`, `b"Bearer token\0"`).
79pub struct Header<'a> {
80 pub name: &'a [u8],
81 pub value: &'a [u8],
82}
83
84// ---------------------------------------------------------------------------
85// ClientStorage
86// ---------------------------------------------------------------------------
87
88/// Backing storage for an HTTP client in zero-heap mode.
89///
90/// In heap mode this is a zero-size placeholder. Pass a `&mut ClientStorage`
91/// to [`Client::create`] — the borrow checker ensures the storage outlives
92/// the client.
93///
94/// ```ignore
95/// let mut storage = ClientStorage::new();
96/// let client = Client::create(&mut storage)?;
97/// ```
98// FFI handle storage; the field is intentionally only addressed via raw
99// pointers passed to C, so clippy's `field is never read` doesn't apply.
100#[allow(dead_code)]
101pub struct ClientStorage(bindings::ove_http_client_storage_t);
102
103impl Default for ClientStorage {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl ClientStorage {
110 /// Zero-initialised backing storage for a [`Client`] in zero-heap
111 /// mode. Place in a `static` and pass to [`Client::from_static`].
112 pub fn new() -> Self {
113 Self(unsafe { core::mem::zeroed() })
114 }
115}
116
117// ---------------------------------------------------------------------------
118// Response
119// ---------------------------------------------------------------------------
120
121/// HTTP response with RAII lifetime for body and header buffers.
122///
123/// Returned by [`Client::get`] and [`Client::post`]. The body and header
124/// memory is freed automatically when this value is dropped.
125pub struct Response {
126 raw: bindings::ove_http_response_t,
127}
128
129impl Response {
130 /// HTTP status code (e.g. 200, 404).
131 pub fn status(&self) -> i32 {
132 self.raw.status
133 }
134
135 /// Response body as a byte slice (empty if no body).
136 pub fn body(&self) -> &[u8] {
137 if self.raw.body.is_null() || self.raw.body_len == 0 {
138 return &[];
139 }
140 // SAFETY: null/zero-len handled above; `body` points to `body_len`
141 // bytes owned by this `Response` and freed only on its `Drop`, so the
142 // slice is valid for `&self`.
143 unsafe { core::slice::from_raw_parts(self.raw.body as *const u8, self.raw.body_len) }
144 }
145
146 /// Raw response headers as a byte slice (empty if none).
147 pub fn headers(&self) -> &[u8] {
148 if self.raw.headers.is_null() || self.raw.headers_len == 0 {
149 return &[];
150 }
151 // SAFETY: null/zero-len handled above; `headers` points to
152 // `headers_len` bytes owned by this `Response` and freed only on its
153 // `Drop`, so the slice is valid for `&self`.
154 unsafe { core::slice::from_raw_parts(self.raw.headers as *const u8, self.raw.headers_len) }
155 }
156}
157
158impl fmt::Debug for Response {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 f.debug_struct("Response")
161 .field("status", &self.raw.status)
162 .field("body_len", &self.raw.body_len)
163 .field("headers_len", &self.raw.headers_len)
164 .finish()
165 }
166}
167
168impl Drop for Response {
169 fn drop(&mut self) {
170 unsafe { bindings::ove_http_response_free(&mut self.raw) }
171 }
172}
173
174// ---------------------------------------------------------------------------
175// Client
176// ---------------------------------------------------------------------------
177
178/// HTTP/1.1 client.
179///
180/// Wraps `ove_http_client_t` with automatic cleanup on drop.
181pub struct Client {
182 handle: bindings::ove_http_client_t,
183}
184
185impl Client {
186 /// Create a new HTTP client via heap allocation (only in heap mode).
187 #[cfg(not(zero_heap))]
188 pub fn new() -> Result<Self> {
189 let mut handle: bindings::ove_http_client_t = core::ptr::null_mut();
190 let rc = unsafe { bindings::ove_http_client_create(&mut handle) };
191 Error::from_code(rc)?;
192 Ok(Self { handle })
193 }
194
195 /// Create from caller-provided static storage.
196 ///
197 /// # Safety
198 /// Caller must ensure `storage` outlives the `Client` and is not
199 /// shared with another primitive.
200 #[cfg(zero_heap)]
201 pub unsafe fn from_static(storage: *mut bindings::ove_http_client_storage_t) -> Result<Self> {
202 let mut handle: bindings::ove_http_client_t = core::ptr::null_mut();
203 let rc = unsafe { bindings::ove_http_client_init(&mut handle, storage) };
204 Error::from_code(rc)?;
205 Ok(Self { handle })
206 }
207
208 /// Create a client that works in both heap and zero-heap modes.
209 ///
210 /// In heap mode, allocates via the RTOS heap. In zero-heap mode,
211 /// uses caller-provided `storage`. The borrow checker ensures the
212 /// storage outlives the client.
213 pub fn create(storage: &mut ClientStorage) -> Result<Self> {
214 #[cfg(not(zero_heap))]
215 {
216 let _ = storage;
217 Self::new()
218 }
219 #[cfg(zero_heap)]
220 {
221 unsafe { Self::from_static(&mut storage.0) }
222 }
223 }
224
225 /// Perform an HTTP GET request.
226 ///
227 /// `url` must be a null-terminated URL (e.g. `b"http://example.com/path\0"`).
228 ///
229 /// # Errors
230 /// Returns an error if the request fails at the transport or protocol level.
231 pub fn get(&self, url: &[u8]) -> Result<Response> {
232 let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
233 let rc = unsafe { bindings::ove_http_get(self.handle, url.as_ptr() as *const _, &mut raw) };
234 Error::from_code(rc)?;
235 Ok(Response { raw })
236 }
237
238 /// Perform an HTTP POST request.
239 ///
240 /// `url` and `content_type` must be null-terminated byte strings.
241 ///
242 /// # Errors
243 /// Returns an error if the request fails at the transport or protocol level.
244 pub fn post(&self, url: &[u8], content_type: &[u8], body: &[u8]) -> Result<Response> {
245 let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
246 let rc = unsafe {
247 bindings::ove_http_post(
248 self.handle,
249 url.as_ptr() as *const _,
250 content_type.as_ptr() as *const _,
251 body.as_ptr() as *const _,
252 body.len(),
253 &mut raw,
254 )
255 };
256 Error::from_code(rc)?;
257 Ok(Response { raw })
258 }
259
260 /// Perform an HTTP request with explicit method, optional body, and
261 /// optional extra headers.
262 ///
263 /// `url` and `content_type` must be null-terminated byte strings.
264 /// Each [`Header`] name/value must also be null-terminated.
265 ///
266 /// # Errors
267 /// Returns an error if the request fails at the transport or protocol level.
268 pub fn request_ex(
269 &self,
270 method: Method,
271 url: &[u8],
272 content_type: &[u8],
273 body: &[u8],
274 headers: &[Header<'_>],
275 ) -> Result<Response> {
276 // Convert Header slices into the C struct array on the stack.
277 const MAX_HEADERS: usize = 16;
278 let count = headers.len().min(MAX_HEADERS);
279 let mut c_headers: [bindings::ove_http_header_t; MAX_HEADERS] =
280 unsafe { core::mem::zeroed() };
281 for i in 0..count {
282 c_headers[i].name = headers[i].name.as_ptr() as *const _;
283 c_headers[i].value = headers[i].value.as_ptr() as *const _;
284 }
285
286 let mut raw: bindings::ove_http_response_t = unsafe { core::mem::zeroed() };
287 let rc = unsafe {
288 bindings::ove_http_request_ex(
289 self.handle,
290 method.to_c(),
291 url.as_ptr() as *const _,
292 content_type.as_ptr() as *const _,
293 body.as_ptr() as *const _,
294 body.len(),
295 c_headers.as_ptr(),
296 count,
297 &mut raw,
298 )
299 };
300 Error::from_code(rc)?;
301 Ok(Response { raw })
302 }
303}
304
305crate::ove_handle_impl!(Client, ove_http_client_destroy, ove_http_client_deinit);