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
use std::alloc::{alloc, Layout};
use crate::properties::{PropertyBooleanValue, PropertyName, PropertyStringValue};
use crate::properties::device_type::DeviceType;
use crate::shim::{mapPropertyToIndex, freeInit, freeResults, fiftyoneDegreesManagerResults, getResultsValue, initToResults, fiftyoneDegreesManagerInit, initToDataset, fiftyoneDegreesDataSetRelease, fiftyoneDegreesHashHighPerformanceConfig, fiftyoneDegreesConfigHash, fiftyoneDegreesDataSetGet, fiftyoneDegreesException, fiftyoneDegreesManagerInitFile, fiftyoneDegreesPropertiesGetRequiredPropertyIndexFromName, fiftyoneDegreesPropertiesRequired, fiftyoneDegreesResourceManager, fiftyoneDegreesResultsHash, fiftyoneDegreesResultsHashCreate, fiftyoneDegreesResultsHashFromUserAgent, fiftyoneDegreesResultsHashGetValuesString, fiftyoneDegreesResultsHashGetValuesStringByRequiredPropertyIndex, fiftyoneDegreesResourceHandleIncUse, fiftyoneDegreesDataSetBase};
use std::ffi::{CString, CStr};
use std::borrow::BorrowMut;
use std::cell::RefCell;
use std::sync::atomic::{AtomicPtr, Ordering};

pub type PropertyIndexes = [i32; 11];

pub struct DeviceDetection {
    init: fiftyoneDegreesManagerInit,
    mapping: PropertyIndexes,
}

impl Drop for DeviceDetection {
    fn drop(&mut self) {
        unsafe {
            freeInit(self.init);
        }
    }
}

unsafe impl Send for DeviceDetection {}

unsafe impl Sync for DeviceDetection {}

pub struct DeviceDetectionResult<'a> {
    mapping: &'a PropertyIndexes,
    results: fiftyoneDegreesManagerResults,
}

impl Drop for DeviceDetectionResult<'_> {
    fn drop(&mut self) {
        unsafe {
            freeResults(self.results);
        }
    }
}

// fiftyoneDegreesResultsHashFree
impl DeviceDetectionResult<'_> {
    pub fn getValueAsInteger(&self, property: &PropertyName) -> std::result::Result<Option<i32>, &str> {
        match self.getValueAsString(property) {
            Ok(value) => match value {
                Some(string) => match string.parse() {
                    Ok(int) => Ok(Some(int)),
                    Err(_) => Err("Unable to convert property string value to int")
                },
                None => Ok(None)
            },
            Err(e) => Err("Unable to get property as string")
        }
    }

    pub fn getValueAsPropertyString(&self, property: &PropertyName) -> std::result::Result<Option<PropertyStringValue>, &str> {
        match self.getValueAsString(property) {
            Ok(value) => match value {
                Some(string) => Ok(PropertyStringValue::new(property, string)),
                None => Ok(None)
            },
            Err(e) => Err("Unable to get property as string")
        }
    }

    pub fn getValueAsString(&self, property: &PropertyName) -> std::result::Result<Option<&str>, &str> {
        let string_ptr = unsafe { getResultsValue(&self.results, self.mapping[usize::from(property)]) };

        if std::ptr::null() == string_ptr {
            return Ok(None);
        }

        let string = unsafe {
            CStr::from_ptr(string_ptr)
        };

        // TODO: use from_utf8_unchecked to remove performance penalty of utf-8 check
        // https://github.com/rust-lang/rust/issues/75196
        match string.to_str() {
            Ok(str) => Ok(Some(str)),
            Err(e) => Err("The pointer returned from C is not a valid utf-8 string")
        }
    }

    pub fn getValueAsBoolean(&self, property: &PropertyName) -> std::result::Result<Option<bool>, &str> {
        match self.getValueAsString(property) {
            Ok(value) => match value {
                Some(string) => {
                    Ok(Some(string.eq("True")))
                }
                None => Ok(None)
            },
            Err(_) => Err("Unable to get property as string")
        }
    }

    pub fn getValueAsPropertyBoolean(&self, property: &PropertyName) -> std::result::Result<Option<PropertyBooleanValue>, &str> {
        let result = self.getValueAsBoolean(property);

        match result {
            Ok(value) => match value {
                Some(bool) => Ok(Some(PropertyBooleanValue::new(property, bool))),
                None => Ok(None)
            },
            Err(_) => Err("Unable to get property as boolean")
        }
    }
}

impl DeviceDetection {
    pub fn new(dataFile: &str, properties: Vec<PropertyName>) -> DeviceDetection {
        let mut converted = Vec::new();

        for property in &properties {
            converted.push(property.as_str());
        }

        let required_properties = CString::new(converted.join(",")).expect("CString::new failed");
        let file_name = CString::new(dataFile).expect("CString::new failed");

        let init = unsafe {
            fiftyoneDegreesManagerInitFile(required_properties.as_ptr(), file_name.as_ptr())
        };

        let mut mapping: PropertyIndexes = [-1; 11];

        for property in &properties {
            let property_string = CString::new(property.as_str()).expect("CString::new failed");

            mapping[usize::from(property)] = unsafe {
                mapPropertyToIndex(&init, property_string.as_ptr())
            };
        }

        DeviceDetection {
            init,
            mapping,
        }
    }

    pub fn lookup(&self, userAgent: &str) -> DeviceDetectionResult {
        let ua_string = CString::new(userAgent).expect("CString::new failed");

        let results = unsafe {
            initToResults(&self.init, ua_string.as_ptr())
        };

        DeviceDetectionResult {
            mapping: &self.mapping,
            results,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::api::DeviceDetection;
    use crate::properties::PropertyName;
    use crate::properties::PropertyName::DeviceType;

    const ua: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) FxiOS/7.5b3349 Mobile/14F89 Safari/603.2.4";

    #[test]
    fn engine() {
        let properties = vec![
            PropertyName::PlatformName,
            PropertyName::BrowserName,
            PropertyName::IsMobile,
            PropertyName::PlatformVersion,
            PropertyName::BrowserVersion
        ];

        let engine = DeviceDetection::new("device-detection-cxx/device-detection-data/51Degrees-LiteV4.1.hash", properties);

        let matched = engine.lookup(ua);

        assert_eq!(matched.getValueAsBoolean(&PropertyName::IsMobile).unwrap().unwrap(), true);
        assert_eq!(matched.getValueAsString(&PropertyName::BrowserName).unwrap().unwrap(), "Firefox for iOS");
        assert_eq!(matched.getValueAsString(&PropertyName::PlatformName).unwrap().unwrap(), "iOS");
        assert_eq!(matched.getValueAsString(&PropertyName::BrowserVersion).unwrap().unwrap(), "7.5");
        assert_eq!(matched.getValueAsString(&PropertyName::PlatformVersion).unwrap().unwrap(), "10.3.2");

        // verify our drop code doesn't cause panics
        drop(matched);
        drop(engine);
    }
}