glean_ffi/upload.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! FFI compatible types for the upload mechanism.
6//!
7//! These are used in the `glean_get_upload_task` and `glean_process_ping_upload_response`
8//! functions.
9
10use std::ffi::CString;
11use std::os::raw::c_char;
12
13use ffi_support::IntoFfi;
14
15use crate::{byte_buffer::ByteBuffer, glean_str_free};
16use glean_core::upload::PingUploadTask;
17
18/// Result values of attempted ping uploads encoded for FFI use.
19///
20/// These are exposed as C `define`s, e.g.:
21///
22/// ```c
23/// #define UPLOAD_RESULT_RECOVERABLE 1
24/// #define UPLOAD_RESULT_UNRECOVERABLE 2
25/// #define UPLOAD_RESULT_HTTP_STATUS 0x8000
26/// ```
27///
28/// The language binding needs to replicate these constants exactly.
29///
30/// The `HTTP_STATUS` result can carry additional data (the HTTP response code).
31/// This is encoded in the lower bits.
32///
33/// The FFI layer can convert from a 32-bit integer (`u32`) representing the upload result (and
34/// associated HTTP response code) into the Glean-compatible `UploadResult` type.
35///
36/// These are defined in `glean-core/src/upload/result.rs`,
37/// but for cbindgen to also export them in header files we need to define them here as constants.
38///
39/// Inline tests ensure they match across crates.
40#[allow(dead_code)]
41pub mod upload_result {
42 /// A recoverable error.
43 pub const UPLOAD_RESULT_RECOVERABLE: u32 = 0x1;
44
45 /// An unrecoverable error.
46 pub const UPLOAD_RESULT_UNRECOVERABLE: u32 = 0x2;
47
48 /// A HTTP response code.
49 ///
50 /// The actual response code is encoded in the lower bits.
51 pub const UPLOAD_RESULT_HTTP_STATUS: u32 = 0x8000;
52}
53
54/// A FFI-compatible representation for the PingUploadTask.
55///
56/// This is exposed as a C-compatible tagged union, like this:
57///
58/// ```c
59/// enum FfiPingUploadTask_Tag {
60/// FfiPingUploadTask_Upload,
61/// FfiPingUploadTask_Wait,
62/// FfiPingUploadTask_Done,
63/// };
64/// typedef uint8_t FfiPingUploadTask_Tag;
65///
66/// typedef struct {
67/// FfiPingUploadTask_Tag tag;
68/// char *document_id;
69/// char *path;
70/// char *body;
71/// char *headers;
72/// } FfiPingUploadTask_Upload_Body;
73///
74/// typedef union {
75/// FfiPingUploadTask_Tag tag;
76/// FfiPingUploadTask_Upload_Body upload;
77/// } FfiPingUploadTask;
78///
79/// ```
80///
81/// It is therefore always valid to read the `tag` field of the returned union (always the first
82/// field in memory).
83///
84/// Language bindings should turn this into proper language types (e.g. enums/structs) and
85/// copy out data.
86///
87/// String fields are encoded into null-terminated UTF-8 C strings.
88///
89/// * The language binding should copy out the data and turn these into their equivalent string type.
90/// * The language binding should _not_ free these fields individually.
91/// Instead `glean_process_ping_upload_response` will receive the whole enum, taking care of
92/// freeing the memory.
93///
94///
95/// The order of variants should be the same as in `glean-core/src/upload/mod.rs`
96/// and `glean-core/android/src/main/java/mozilla/telemetry/glean/net/Upload.kt`.
97///
98/// cbindgen:prefix-with-name
99#[repr(u8)]
100pub enum FfiPingUploadTask {
101 Upload {
102 document_id: *mut c_char,
103 path: *mut c_char,
104 body: ByteBuffer,
105 headers: *mut c_char,
106 },
107 Wait(u64),
108 Done,
109}
110
111impl From<PingUploadTask> for FfiPingUploadTask {
112 fn from(task: PingUploadTask) -> Self {
113 match task {
114 PingUploadTask::Upload(request) => {
115 // Safe unwraps:
116 // 1. CString::new(..) should not fail as we are the ones that created the strings being transformed;
117 // 2. serde_json::to_string(&request.headers) should not fail as request.headers is a HashMap of Strings.
118 let document_id = CString::new(request.document_id.to_owned()).unwrap();
119 let path = CString::new(request.path.to_owned()).unwrap();
120 let headers =
121 CString::new(serde_json::to_string(&request.headers).unwrap()).unwrap();
122 FfiPingUploadTask::Upload {
123 document_id: document_id.into_raw(),
124 path: path.into_raw(),
125 body: ByteBuffer::from_vec(request.body),
126 headers: headers.into_raw(),
127 }
128 }
129 PingUploadTask::Wait(time) => FfiPingUploadTask::Wait(time),
130 PingUploadTask::Done => FfiPingUploadTask::Done,
131 }
132 }
133}
134
135impl Drop for FfiPingUploadTask {
136 fn drop(&mut self) {
137 if let FfiPingUploadTask::Upload {
138 document_id,
139 path,
140 body,
141 headers,
142 } = self
143 {
144 // We need to free the previously allocated strings before dropping.
145 unsafe {
146 glean_str_free(*document_id);
147 glean_str_free(*path);
148 glean_str_free(*headers);
149 }
150 // Unfortunately, we cannot directly call `body.destroy();` as
151 // we're behind a mutable reference, so we have to manually take the
152 // ownership and drop. Moreover, `ByteBuffer::new_with_size(0)`
153 // does not allocate, so we are not leaking memory.
154 let body = std::mem::replace(body, ByteBuffer::new_with_size(0));
155 body.destroy();
156 }
157 }
158}
159
160unsafe impl IntoFfi for FfiPingUploadTask {
161 type Value = FfiPingUploadTask;
162
163 #[inline]
164 fn ffi_default() -> FfiPingUploadTask {
165 FfiPingUploadTask::Done
166 }
167
168 #[inline]
169 fn into_ffi_value(self) -> FfiPingUploadTask {
170 self
171 }
172}
173
174#[cfg(test)]
175mod test {
176 use super::*;
177
178 #[test]
179 fn constants_match_with_glean_core() {
180 assert_eq!(
181 upload_result::UPLOAD_RESULT_RECOVERABLE,
182 glean_core::upload::ffi_upload_result::UPLOAD_RESULT_RECOVERABLE
183 );
184 assert_eq!(
185 upload_result::UPLOAD_RESULT_UNRECOVERABLE,
186 glean_core::upload::ffi_upload_result::UPLOAD_RESULT_UNRECOVERABLE
187 );
188 assert_eq!(
189 upload_result::UPLOAD_RESULT_HTTP_STATUS,
190 glean_core::upload::ffi_upload_result::UPLOAD_RESULT_HTTP_STATUS
191 );
192 }
193}