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 {}