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}