1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! FFI compatible types for the upload mechanism.
//!
//! These are used in the `glean_get_upload_task` and `glean_process_ping_upload_response`
//! functions.

use std::ffi::CString;
use std::os::raw::c_char;

use ffi_support::{ByteBuffer, IntoFfi};

use crate::glean_str_free;
use glean_core::upload::PingUploadTask;

/// Result values of attempted ping uploads encoded for FFI use.
///
/// These are exposed as C `define`s, e.g.:
///
/// ```c
/// #define UPLOAD_RESULT_RECOVERABLE 1
/// #define UPLOAD_RESULT_UNRECOVERABLE 2
/// #define UPLOAD_RESULT_HTTP_STATUS 0x8000
/// ```
///
/// The language binding needs to replicate these constants exactly.
///
/// The `HTTP_STATUS` result can carry additional data (the HTTP response code).
/// This is encoded in the lower bits.
///
/// The FFI layer can convert from a 32-bit integer (`u32`) representing the upload result (and
/// associated HTTP response code) into the Glean-compatible `UploadResult` type.
///
/// These are defined in `glean-core/src/upload/result.rs`,
/// but for cbindgen to also export them in header files we need to define them here as constants.
///
/// Inline tests ensure they match across crates.
#[allow(dead_code)]
pub mod upload_result {
    /// A recoverable error.
    pub const UPLOAD_RESULT_RECOVERABLE: u32 = 0x1;

    /// An unrecoverable error.
    pub const UPLOAD_RESULT_UNRECOVERABLE: u32 = 0x2;

    /// A HTTP response code.
    ///
    /// The actual response code is encoded in the lower bits.
    pub const UPLOAD_RESULT_HTTP_STATUS: u32 = 0x8000;
}

/// A FFI-compatible representation for the PingUploadTask.
///
/// This is exposed as a C-compatible tagged union, like this:
///
/// ```c
/// enum FfiPingUploadTask_Tag {
///   FfiPingUploadTask_Upload,
///   FfiPingUploadTask_Wait,
///   FfiPingUploadTask_Done,
/// };
/// typedef uint8_t FfiPingUploadTask_Tag;
///
/// typedef struct {
///   FfiPingUploadTask_Tag tag;
///   char *document_id;
///   char *path;
///   char *body;
///   char *headers;
/// } FfiPingUploadTask_Upload_Body;
///
/// typedef union {
///   FfiPingUploadTask_Tag tag;
///   FfiPingUploadTask_Upload_Body upload;
/// } FfiPingUploadTask;
///
/// ```
///
/// It is therefore always valid to read the `tag` field of the returned union (always the first
/// field in memory).
///
/// Language bindings should turn this into proper language types (e.g. enums/structs) and
/// copy out data.
///
/// String fields are encoded into null-terminated UTF-8 C strings.
///
/// * The language binding should copy out the data and turn these into their equivalent string type.
/// * The language binding should _not_ free these fields individually.
///   Instead `glean_process_ping_upload_response` will receive the whole enum, taking care of
///   freeing the memory.
///
///
/// The order of variants should be the same as in `glean-core/src/upload/mod.rs`
/// and `glean-core/android/src/main/java/mozilla/telemetry/glean/net/Upload.kt`.
///
/// cbindgen:prefix-with-name
#[repr(u8)]
pub enum FfiPingUploadTask {
    Upload {
        document_id: *mut c_char,
        path: *mut c_char,
        body: ByteBuffer,
        headers: *mut c_char,
    },
    Wait,
    Done,
}

impl From<PingUploadTask> for FfiPingUploadTask {
    fn from(task: PingUploadTask) -> Self {
        match task {
            PingUploadTask::Upload(request) => {
                // Safe unwraps:
                // 1. CString::new(..) should not fail as we are the ones that created the strings being transformed;
                // 2. serde_json::to_string(&request.headers) should not fail as request.headers is a HashMap of Strings.
                let document_id = CString::new(request.document_id.to_owned()).unwrap();
                let path = CString::new(request.path.to_owned()).unwrap();
                let headers =
                    CString::new(serde_json::to_string(&request.headers).unwrap()).unwrap();
                FfiPingUploadTask::Upload {
                    document_id: document_id.into_raw(),
                    path: path.into_raw(),
                    body: ByteBuffer::from_vec(request.body),
                    headers: headers.into_raw(),
                }
            }
            PingUploadTask::Wait => FfiPingUploadTask::Wait,
            PingUploadTask::Done => FfiPingUploadTask::Done,
        }
    }
}

impl Drop for FfiPingUploadTask {
    fn drop(&mut self) {
        if let FfiPingUploadTask::Upload {
            document_id,
            path,
            body,
            headers,
        } = self
        {
            // We need to free the previously allocated strings before dropping.
            unsafe {
                glean_str_free(*document_id);
                glean_str_free(*path);
                glean_str_free(*headers);
            }
            // Unfortunately, we cannot directly call `body.destroy();` as
            // we're behind a mutable reference, so we have to manually take the
            // ownership and drop. Moreover, `ByteBuffer::new_with_size(0)`
            // does not allocate, so we are not leaking memory.
            let body = std::mem::replace(body, ByteBuffer::new_with_size(0));
            body.destroy();
        }
    }
}

unsafe impl IntoFfi for FfiPingUploadTask {
    type Value = FfiPingUploadTask;

    #[inline]
    fn ffi_default() -> FfiPingUploadTask {
        FfiPingUploadTask::Done
    }

    #[inline]
    fn into_ffi_value(self) -> FfiPingUploadTask {
        self
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn constants_match_with_glean_core() {
        assert_eq!(
            upload_result::UPLOAD_RESULT_RECOVERABLE,
            glean_core::upload::ffi_upload_result::UPLOAD_RESULT_RECOVERABLE
        );
        assert_eq!(
            upload_result::UPLOAD_RESULT_UNRECOVERABLE,
            glean_core::upload::ffi_upload_result::UPLOAD_RESULT_UNRECOVERABLE
        );
        assert_eq!(
            upload_result::UPLOAD_RESULT_HTTP_STATUS,
            glean_core::upload::ffi_upload_result::UPLOAD_RESULT_HTTP_STATUS
        );
    }
}