fast_browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5cf1f07c8cf5352d87676a1bf461dc53f7881120
4
+ data.tar.gz: 9d3d924ef18bfac7948dd3acc197c56d5813d457
5
+ SHA512:
6
+ metadata.gz: 2f8e15193b73056ad463a242d8ff063e01971e2097ea7bcc65d243a585d409654fae0f05f3a22e184699db05808720c4666f50461d33b43acebabd9c2773e6ad
7
+ data.tar.gz: 13db7ca269f9f541af9bd18873d37a6627ad42b8b193648a4c7f1e3d2a42d931a250ca1ae4ae2772e8d654f1ea9307028ca572f5c8785145ab389e1db171000c
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ Cargo.lock
3
+ Makefile
4
+ mkmf.log
5
+ ext/fast_browser
6
+ pkg
7
+ rust/target
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,29 @@
1
+ language: rust
2
+
3
+ rust:
4
+ - beta
5
+ - nightly
6
+
7
+ matrix:
8
+ allow_failures:
9
+ - rust: nightly
10
+
11
+ before_install:
12
+ - rvm install 2.3.0
13
+ - gem install bundler
14
+ - bundle install
15
+
16
+ install:
17
+ - ruby ext/extconf.rb
18
+ - make
19
+
20
+ script:
21
+ # Rust tests
22
+ - cd $TRAVIS_BUILD_DIR/rust
23
+ - cargo test
24
+ # Ruby specs
25
+ - cd $TRAVIS_BUILD_DIR
26
+ - bundle exec rspec
27
+ # Check that the gem builds and installs
28
+ - bundle exec rake install
29
+ - ruby -e "require 'rubygems'; require 'fast_browser'"
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,37 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ fast_browser (0.1.0)
5
+ ffi (~> 1.9.10)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.2.5)
11
+ ffi (1.9.10)
12
+ rake (10.4.2)
13
+ rspec (3.4.0)
14
+ rspec-core (~> 3.4.0)
15
+ rspec-expectations (~> 3.4.0)
16
+ rspec-mocks (~> 3.4.0)
17
+ rspec-core (3.4.1)
18
+ rspec-support (~> 3.4.0)
19
+ rspec-expectations (3.4.0)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.4.0)
22
+ rspec-mocks (3.4.1)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.4.0)
25
+ rspec-support (3.4.1)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ bundler (~> 1.10)
32
+ fast_browser!
33
+ rake (~> 10.4.2)
34
+ rspec (~> 3.4.0)
35
+
36
+ BUNDLED WITH
37
+ 1.11.2
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/ext/extconf.rb ADDED
@@ -0,0 +1,39 @@
1
+ require 'mkmf'
2
+
3
+ def sys(cmd, &block)
4
+ block = ->(f) { f.gets } if block.nil?
5
+
6
+ ret = IO.popen(cmd, &block)
7
+
8
+ if $?.to_i != 0
9
+ puts "=> Failed!"
10
+ raise "Command failed: #{cmd}"
11
+ else
12
+ ret
13
+ end
14
+ end
15
+
16
+ ROOT = File.expand_path '..', File.dirname(__FILE__)
17
+ RUST_ROOT = File.join ROOT, 'rust'
18
+
19
+ puts ' - Checking Rust compiler'
20
+ rustc = sys "cd #{RUST_ROOT}; rustc --version"
21
+
22
+ if !(rustc.include?('rustc 1.6.0') || rustc.include?('rustc 1.7.0'))
23
+ puts "=> Bad Rust compiler version: #{rustc}"
24
+ puts ' Version 1.6.0 or 1.7.0 is required.'
25
+
26
+ raise "Invalid Rust compiler version"
27
+ end
28
+
29
+ # Create an empty makefile with an empty `install` task
30
+ puts ' - Creating Makefile'
31
+ File.open('Makefile', 'w') do |f|
32
+ body = [
33
+ "install:",
34
+ "\tcd #{RUST_ROOT}; cargo build --release",
35
+ "\tmkdir -p #{ROOT}/ext/fast_browser",
36
+ "\tcp #{RUST_ROOT}/target/release/libfast_browser.* #{ROOT}/ext/fast_browser"
37
+ ]
38
+ f.puts(body.join("\n") + "\n")
39
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'fast_browser/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'fast_browser'
7
+ s.version = FastBrowser::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Dirk Gadsden']
10
+ s.email = ['dirk@esherido.com']
11
+ s.homepage = 'https://github.com/dirk/fast_browser'
12
+ s.summary = 'Blazing-fast, Rust-powered user agent detection library.'
13
+ s.description = s.summary
14
+ s.license = ''
15
+
16
+ s.files = `git ls-files`.split "\n"
17
+ s.test_files = `git ls-files -- {test}/*`.split "\n"
18
+ s.require_paths = ['lib']
19
+ s.extensions = ['ext/extconf.rb']
20
+
21
+ s.add_dependency 'ffi', '~> 1.9.10'
22
+
23
+ s.add_development_dependency 'bundler', '~> 1.10'
24
+ s.add_development_dependency 'rake', '~> 10.4.2'
25
+ s.add_development_dependency 'rspec', '~> 3.4.0'
26
+ end
@@ -0,0 +1,15 @@
1
+ class FastBrowser
2
+ module LibraryExtensions
3
+ def attach_string_returning_function(name, arg_types)
4
+ private_name = "_#{name}".to_sym
5
+
6
+ attach_function private_name, name, arg_types, :strptr
7
+
8
+ class_eval <<-BODY
9
+ def self.#{name}(*args)
10
+ call_and_free_string :#{private_name}, *args
11
+ end
12
+ BODY
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ class FastBrowser
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,69 @@
1
+ require 'ffi'
2
+
3
+ require 'fast_browser/version'
4
+ require 'fast_browser/library_extensions'
5
+
6
+ class FastBrowser
7
+ module RustLib
8
+ extend FFI::Library
9
+ extend LibraryExtensions
10
+
11
+ lib_file = "libfast_browser.#{FFI::Platform::LIBSUFFIX}"
12
+ ffi_lib File.expand_path("../../ext/fast_browser/#{lib_file}", __FILE__)
13
+
14
+ %w(chrome edge firefox opera safari).each do |tester|
15
+ attach_function "is_#{tester}".to_sym, [:pointer], :bool
16
+ end
17
+
18
+ attach_function :get_browser_minor_version, [:pointer], :int8
19
+ attach_function :get_browser_major_version, [:pointer], :int8
20
+ attach_function :is_mobile, [:pointer], :bool
21
+
22
+ attach_string_returning_function :get_browser_family, [:pointer]
23
+ attach_string_returning_function :get_user_agent, [:pointer]
24
+ attach_string_returning_function :get_version, []
25
+
26
+ # Private Rust methods; don't call these directly!
27
+ attach_function :_parse_user_agent, :parse_user_agent, [:string], :pointer
28
+ attach_function :_free_user_agent, :free_user_agent, [:pointer], :void
29
+ attach_function :_free_string, :free_string, [:pointer], :void
30
+
31
+ # Sends the given method name (`method`) to self, copies the returned
32
+ # string into a Ruby string and then calls `.free_string` to deallocate
33
+ # the original returned string.
34
+ def self.call_and_free_string method, *args
35
+ string, ptr = send method, *args
36
+ _free_string ptr
37
+ string
38
+ end
39
+
40
+ def self.parse_user_agent string
41
+ FFI::AutoPointer.new(
42
+ self._parse_user_agent(string),
43
+ self.method(:_free_user_agent)
44
+ )
45
+ end
46
+ end
47
+
48
+ def initialize(string)
49
+ @pointer = RustLib.parse_user_agent(string)
50
+ end
51
+
52
+ def chrome?; RustLib.is_chrome(@pointer) end
53
+ def edge?; RustLib.is_edge(@pointer) end
54
+ def firefox?; RustLib.is_firefox(@pointer) end
55
+ def opera?; RustLib.is_opera(@pointer) end
56
+ def safari?; RustLib.is_safari(@pointer) end
57
+ def mobile?; RustLib.is_mobile(@pointer) end
58
+
59
+ def major_version; RustLib.get_browser_major_version(@pointer) end
60
+ def minor_version; RustLib.get_browser_minor_version(@pointer) end
61
+ def family; RustLib.get_browser_family(@pointer) end
62
+ def user_agent; RustLib.get_user_agent(@pointer) end
63
+ end
64
+
65
+ if FastBrowser::RustLib.get_version != FastBrowser::VERSION
66
+ e = FastBrowser::VERSION
67
+ g = FastBrowser::RustLib.get_version
68
+ raise "Rust library version doesn't match Ruby gem version (expected #{e}, got #{g})"
69
+ end
data/rust/Cargo.toml ADDED
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "fast_browser"
3
+ version = "0.0.1"
4
+ authors = []
5
+
6
+ [lib]
7
+ name = "fast_browser"
8
+ crate-type = ["dylib"]
9
+
10
+ [dependencies]
11
+ lazy_static = "0.1.*"
12
+ libc = "0.2"
13
+ regex = "0.1.47"
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+
3
+ # Call this like `TARGET=release ./build-debug.sh` to do a release build
4
+
5
+ # TODO: Make this set the right extension based on the current platform
6
+ NATIVE_EXT=dylib
7
+
8
+ if [ "$TARGET" == "release" ]; then
9
+ CARGO_ARGS=--release
10
+ TARGET=release
11
+ else
12
+ CARGO_ARGS=
13
+ TARGET=debug
14
+ fi
15
+
16
+ set -x
17
+ cargo build $CARGO_ARGS && cp target/$TARGET/libfast_browser.$NATIVE_EXT ../ext/fast_browser/libfast_browser.$NATIVE_EXT
data/rust/src/bot.rs ADDED
@@ -0,0 +1,66 @@
1
+ use regex::{Regex};
2
+ use util::map_first_captures;
3
+
4
+ #[derive(Debug, PartialEq)]
5
+ pub enum BotName {
6
+ Googlebot,
7
+ }
8
+
9
+ #[derive(Debug, PartialEq)]
10
+ pub struct Bot {
11
+ pub name: BotName,
12
+ }
13
+
14
+ impl Bot {
15
+ pub fn new(name: BotName) -> Bot {
16
+ Bot { name: name, }
17
+ }
18
+
19
+ pub fn parse(ua: &str) -> Option<Bot> {
20
+ for matcher in MATCH_SEQUENCE.iter() {
21
+ if let Some(bot) = matcher(ua) {
22
+ return Some(bot)
23
+ }
24
+ }
25
+
26
+ None
27
+ }
28
+
29
+ fn match_googlebot(ua: &str) -> Option<Bot> {
30
+ let versions = GOOGLEBOT_REGEX
31
+ .captures(ua)
32
+ .map(map_first_captures);
33
+
34
+ if let Some(_) = versions {
35
+ Some(Bot::new(BotName::Googlebot))
36
+ } else {
37
+ None
38
+ }
39
+ }
40
+ }
41
+
42
+ type MatcherFn = Fn(&str) -> Option<Bot> + Sync;
43
+ type Matcher = Box<MatcherFn>;
44
+
45
+ lazy_static! {
46
+ static ref GOOGLEBOT_REGEX: Regex = Regex::new(r"Googlebot/(\d+)\.(\d+)").unwrap();
47
+
48
+ static ref MATCH_SEQUENCE: Vec<Matcher> = vec![
49
+ Box::new(Bot::match_googlebot),
50
+ ];
51
+ }
52
+
53
+ #[cfg(test)]
54
+ mod tests {
55
+ use super::{Bot, BotName};
56
+
57
+ const GOOGLEBOT: &'static str = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)";
58
+
59
+ #[test]
60
+ fn test_parse_googlebot() {
61
+ assert_eq!(
62
+ Bot::new(BotName::Googlebot),
63
+ Bot::parse(GOOGLEBOT).unwrap()
64
+ )
65
+ }
66
+ }
@@ -0,0 +1,181 @@
1
+ use regex::{Regex};
2
+
3
+ use util::map_first_captures;
4
+
5
+ #[derive(Clone, Debug, PartialEq)]
6
+ pub enum BrowserFamily {
7
+ Chrome,
8
+ Edge,
9
+ Firefox,
10
+ Opera,
11
+ Safari,
12
+ MobileSafari,
13
+ }
14
+
15
+ impl BrowserFamily {
16
+ pub fn is_mobile(&self) -> bool {
17
+ match self {
18
+ &BrowserFamily::MobileSafari => true,
19
+ _ => false,
20
+ }
21
+ }
22
+ }
23
+
24
+ #[derive(Clone, Debug, PartialEq)]
25
+ pub struct Browser {
26
+ pub family: BrowserFamily,
27
+ pub major_version: i8,
28
+ pub minor_version: i8,
29
+ }
30
+
31
+ type MatcherFn = Fn(&str) -> Option<(i8, i8)> + Sync;
32
+ type Matcher = (BrowserFamily, Box<MatcherFn>);
33
+
34
+ lazy_static! {
35
+ // NOTE: Order of tests is significant
36
+ static ref MATCH_SEQUENCE: Vec<Matcher> = vec![
37
+ (BrowserFamily::Opera, Box::new(Browser::match_opera)),
38
+ (BrowserFamily::Edge, Box::new(Browser::match_edge)),
39
+ (BrowserFamily::Chrome, Box::new(Browser::match_chrome)),
40
+ (BrowserFamily::Firefox, Box::new(Browser::match_firefox)),
41
+ (BrowserFamily::MobileSafari, Box::new(Browser::match_mobile_safari)),
42
+ (BrowserFamily::Safari, Box::new(Browser::match_safari)),
43
+ ];
44
+ }
45
+
46
+ impl Browser {
47
+ fn new(family: BrowserFamily, versions: (i8, i8)) -> Browser {
48
+ Browser {
49
+ family: family,
50
+ major_version: versions.0,
51
+ minor_version: versions.1,
52
+ }
53
+ }
54
+
55
+ #[allow(unused_parens)]
56
+ pub fn parse(ua: &str) -> Option<Browser> {
57
+ // NOTE: Order of these tests is significant because browser vendors are terrible
58
+
59
+ for tuple in MATCH_SEQUENCE.iter() {
60
+ let &(ref family, ref matcher) = tuple;
61
+
62
+ if let Some(versions) = matcher(ua) {
63
+ let browser = Browser::new(family.clone(), versions);
64
+
65
+ return Some(browser)
66
+ }
67
+ }
68
+
69
+ return None
70
+ }
71
+
72
+ /// Take a regex and attempt to match it to the browser. The regex must include two capture
73
+ /// groups that capture the version of the matched browser.
74
+ fn match_versions(ua: &str, regex: &Regex) -> Option<(i8, i8)> {
75
+ regex
76
+ .captures(ua)
77
+ .map(map_first_captures)
78
+ }
79
+ }
80
+
81
+ lazy_static! {
82
+ static ref CHROME_REGEX: Regex = Regex::new(r"Chrom(?:ium|e)/(\d+)\.(\d+)").unwrap();
83
+ static ref EDGE_REGEX: Regex = Regex::new(r"Edge/(\d+)\.(\d+)").unwrap();
84
+ static ref FIREFOX_REGEX: Regex = Regex::new(r"Firefox/(\d+)\.(\d+)").unwrap();
85
+ static ref OPERA_VERSION_REGEX: Regex = Regex::new(r"Version/(\d+)\.(\d+)").unwrap();
86
+ static ref SAFARI_VERSION_REGEX: Regex = Regex::new(r"Version/(\d+)\.(\d+)").unwrap();
87
+ }
88
+
89
+ impl Browser {
90
+ pub fn match_edge(ua: &str) -> Option<(i8, i8)> {
91
+ Browser::match_versions(ua, &EDGE_REGEX)
92
+ }
93
+
94
+ /// Search for the Firefox componenet in the user agent and parse out the version if present
95
+ pub fn match_firefox(ua: &str) -> Option<(i8, i8)> {
96
+ Browser::match_versions(ua, &FIREFOX_REGEX)
97
+ }
98
+
99
+ pub fn match_chrome(ua: &str) -> Option<(i8, i8)> {
100
+ Browser::match_versions(ua, &CHROME_REGEX)
101
+ }
102
+
103
+ pub fn match_opera(ua: &str) -> Option<(i8, i8)> {
104
+ if !ua.contains("Opera") { return None }
105
+
106
+ Browser::match_versions(ua, &OPERA_VERSION_REGEX)
107
+ }
108
+
109
+ pub fn match_safari(ua: &str) -> Option<(i8, i8)> {
110
+ if !ua.contains("Safari") { return None }
111
+ if ua.contains("Mobile/") { return None }
112
+
113
+ Browser::match_versions(ua, &SAFARI_VERSION_REGEX)
114
+ }
115
+
116
+ pub fn match_mobile_safari(ua: &str) -> Option<(i8, i8)> {
117
+ if !ua.contains("Safari") { return None }
118
+ if !ua.contains("Mobile/") { return None }
119
+
120
+ Browser::match_versions(ua, &SAFARI_VERSION_REGEX)
121
+ }
122
+ }
123
+
124
+ #[cfg(test)]
125
+ mod tests {
126
+ use super::{Browser, BrowserFamily};
127
+
128
+ type StaticStr = &'static str;
129
+
130
+ const OPERA_12: StaticStr = "Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16";
131
+ const OPERA_11: StaticStr = "Opera/9.80 (Windows NT 6.1; WOW64; U; pt) Presto/2.10.229 Version/11.62";
132
+ const SAFARI_7: StaticStr = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A";
133
+ const SAFARI_5: StaticStr = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us) AppleWebKit/534.1+ (KHTML, like Gecko) Version/5.0 Safari/533.16";
134
+ const MOBILE_SAFARI_6: StaticStr = "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25";
135
+
136
+ #[test]
137
+ fn test_parse_safari() {
138
+ assert_eq!(
139
+ Browser::new(BrowserFamily::Safari, (7, 0)),
140
+ Browser::parse(SAFARI_7).unwrap()
141
+ );
142
+
143
+ assert_eq!(
144
+ Browser::new(BrowserFamily::MobileSafari, (6, 0)),
145
+ Browser::parse(MOBILE_SAFARI_6).unwrap()
146
+ )
147
+ }
148
+
149
+ #[test]
150
+ fn test_match_firefox() {
151
+ let did_match = Browser::match_firefox("Firefox/1.2");
152
+ assert_eq!(did_match, Some((1, 2)));
153
+
154
+ let didnt_match = Browser::match_firefox("NotFirefox/x.y");
155
+ assert_eq!(didnt_match, None)
156
+ }
157
+
158
+ #[test]
159
+ fn test_match_safari() {
160
+ let version_7 = Browser::match_safari(SAFARI_7);
161
+ assert_eq!(version_7, Some((7, 0)));
162
+
163
+ let version_5 = Browser::match_safari(SAFARI_5);
164
+ assert_eq!(version_5, Some((5, 0)));
165
+ }
166
+
167
+ #[test]
168
+ fn test_match_mobile_safari() {
169
+ let mobile_version_6 = Browser::match_mobile_safari(MOBILE_SAFARI_6);
170
+ assert_eq!(mobile_version_6, Some((6, 0)))
171
+ }
172
+
173
+ #[test]
174
+ fn test_match_opera() {
175
+ let opera_12 = Browser::match_opera(OPERA_12);
176
+ assert_eq!(opera_12, Some((12, 16)));
177
+
178
+ let opera_11 = Browser::match_opera(OPERA_11);
179
+ assert_eq!(opera_11, Some((11, 62)))
180
+ }
181
+ }
data/rust/src/lib.rs ADDED
@@ -0,0 +1,109 @@
1
+ #[macro_use]
2
+ extern crate lazy_static;
3
+
4
+ extern crate libc;
5
+ extern crate regex;
6
+
7
+ mod bot;
8
+ mod browser;
9
+ mod user_agent;
10
+ mod util;
11
+
12
+ use libc::c_char;
13
+ use std::ffi::{CStr, CString};
14
+ use self::browser::BrowserFamily;
15
+ use self::user_agent::UserAgent;
16
+
17
+ #[no_mangle]
18
+ pub extern fn parse_user_agent(cstring: *const c_char) -> *const UserAgent {
19
+ let string = unsafe { CStr::from_ptr(cstring) }.to_str().unwrap();
20
+ let browser = UserAgent::parse(string);
21
+
22
+ Box::into_raw(Box::new(browser))
23
+ }
24
+
25
+ /// Take back ownership of an externally-owned `Browser` and destructively deallocate it.
26
+ #[no_mangle]
27
+ pub extern fn free_user_agent(ua: *mut UserAgent) {
28
+ drop(unsafe { Box::from_raw(ua) })
29
+ }
30
+
31
+ macro_rules! is_family {
32
+ ($function:ident, $family:path) => {
33
+ #[no_mangle]
34
+ pub extern fn $function(ua: *const UserAgent) -> bool {
35
+ if let Some(ref browser) = UserAgent::borrow_from_c(ua).browser {
36
+ browser.family == $family
37
+ } else {
38
+ false
39
+ }
40
+ }
41
+ };
42
+ }
43
+
44
+ is_family!(is_chrome, BrowserFamily::Chrome);
45
+ is_family!(is_edge, BrowserFamily::Edge);
46
+ is_family!(is_firefox, BrowserFamily::Firefox);
47
+ is_family!(is_opera, BrowserFamily::Opera);
48
+ is_family!(is_safari, BrowserFamily::Safari);
49
+
50
+ #[no_mangle]
51
+ pub extern fn is_mobile(ua: *const UserAgent) -> bool {
52
+ let ua = UserAgent::borrow_from_c(ua);
53
+
54
+ match ua.browser {
55
+ Some(ref b) => b.family.is_mobile(),
56
+ _ => false
57
+ }
58
+ }
59
+
60
+ #[no_mangle]
61
+ pub extern fn get_browser_major_version(ua: *const UserAgent) -> i8 {
62
+ UserAgent::borrow_from_c(ua).browser.clone().map_or(0, |b| b.major_version)
63
+ }
64
+
65
+ #[no_mangle]
66
+ pub extern fn get_browser_minor_version(ua: *const UserAgent) -> i8 {
67
+ UserAgent::borrow_from_c(ua).browser.clone().map_or(0, |b| b.minor_version)
68
+ }
69
+
70
+ /// Returns the user agent's browser family name as a heap-allocated `CString`
71
+ #[no_mangle]
72
+ pub extern fn get_browser_family(ua: *const UserAgent) -> *mut c_char {
73
+ let browser = UserAgent::borrow_from_c(ua).browser.clone();
74
+
75
+ let family =
76
+ browser.map_or("Other", |browser| {
77
+ match browser.family {
78
+ BrowserFamily::Chrome => "Chrome",
79
+ BrowserFamily::Edge => "Edge",
80
+ BrowserFamily::Firefox => "Firefox",
81
+ BrowserFamily::Opera => "Opera",
82
+ BrowserFamily::Safari => "Safari",
83
+ BrowserFamily::MobileSafari => "Mobile Safari",
84
+ }
85
+ });
86
+
87
+ CString::new(family).unwrap().into_raw()
88
+ }
89
+
90
+ /// Returns the original user agent that was parsed as a `CString` (must free later)
91
+ #[no_mangle]
92
+ pub extern fn get_user_agent(ua: *const UserAgent) -> *mut c_char {
93
+ let ref ua = UserAgent::borrow_from_c(ua);
94
+
95
+ CString::new(ua.source.clone()).unwrap().into_raw()
96
+ }
97
+
98
+ /// Free a `CString` pointer owned by Rust
99
+ #[no_mangle]
100
+ pub extern fn free_string(string: *mut c_char) {
101
+ drop(unsafe { CString::from_raw(string) })
102
+ }
103
+
104
+ const VERSION: &'static str = "0.0.1";
105
+
106
+ #[no_mangle]
107
+ pub extern fn get_version() -> *const c_char {
108
+ CString::new(VERSION).unwrap().into_raw()
109
+ }
@@ -0,0 +1,32 @@
1
+ use std::mem;
2
+ use bot::Bot;
3
+ use browser::Browser;
4
+
5
+ pub struct UserAgent {
6
+ pub browser: Option<Browser>,
7
+ pub bot: Option<Bot>,
8
+
9
+ /// The string that was parsed to determine the browser, bot, etc.
10
+ pub source: String,
11
+ }
12
+
13
+ impl UserAgent {
14
+ pub fn parse(ua: &str) -> UserAgent {
15
+ let browser = Browser::parse(ua);
16
+ let bot = Bot::parse(ua);
17
+
18
+ UserAgent {
19
+ browser: browser,
20
+ bot: bot,
21
+ source: ua.to_owned(),
22
+ }
23
+ }
24
+
25
+ /// Take an externally-owned `Browser` and non-destructively borrow a reference to it.
26
+ ///
27
+ /// **Note**: This will *not* deallocate the instance passed in. So it is safe to call this
28
+ /// over and over again.
29
+ pub fn borrow_from_c<'a>(ua: *const UserAgent) -> &'a UserAgent {
30
+ unsafe { mem::transmute(ua) }
31
+ }
32
+ }
data/rust/src/util.rs ADDED
@@ -0,0 +1,10 @@
1
+ use regex::{Captures};
2
+ use std::str::FromStr;
3
+
4
+ /// Takes the first two capture groups from a regex result and turns them into a version
5
+ /// integer 2-tuple
6
+ pub fn map_first_captures(captures: Captures) -> (i8, i8) {
7
+ let major_version = i8::from_str(&captures[1]).unwrap();
8
+ let minor_version = i8::from_str(&captures[2]).unwrap();
9
+ (major_version, minor_version)
10
+ }
data/spec/name_spec.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastBrowser do
4
+ let(:firefox) { 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1' }
5
+ let(:mobile_safari) { 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25' }
6
+
7
+ it 'parses Firefox name' do
8
+ browser = FastBrowser.new firefox
9
+
10
+ expect(browser.family).to eq 'Firefox'
11
+ end
12
+
13
+ it 'parses Chrome name' do
14
+ browser = FastBrowser.new 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36'
15
+
16
+ expect(browser.family).to eq 'Chrome'
17
+ end
18
+
19
+ it 'returns Other if not parses matches' do
20
+ browser = FastBrowser.new 'Mozilla/5.0 Unknwon/1.2'
21
+
22
+ expect(browser.family).to eq 'Other'
23
+ end
24
+
25
+ it 'returns true if it is a mobile browser' do
26
+ browser = FastBrowser.new mobile_safari
27
+
28
+ expect(browser.mobile?).to eq true
29
+ end
30
+
31
+ it "returns false if it isn't a mobile browser" do
32
+ browser = FastBrowser.new firefox
33
+
34
+ expect(browser.mobile?).to eq false
35
+ end
36
+
37
+ describe '#user_agent' do
38
+ it 'returns the original user agent' do
39
+ nonsense = 'abc123'
40
+ browser = FastBrowser.new nonsense
41
+
42
+ expect(browser.user_agent).to eq nonsense
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastBrowser do
4
+ let(:firefox) { 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1' }
5
+ let(:chrome) { 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36' }
6
+
7
+ it 'seems to be memory-safe' do
8
+ b1 = FastBrowser.new firefox
9
+ expect(b1.firefox?).to eq true
10
+ expect(b1.chrome?).to eq false
11
+
12
+ b2 = FastBrowser.new chrome
13
+ expect(b2.firefox?).to eq false
14
+ expect(b2.chrome?).to eq true
15
+
16
+ # Then check each against to make sure they didn't overwrite each other
17
+ # or something
18
+
19
+ expect(b1.firefox?).to eq true
20
+ expect(b1.chrome?).to eq false
21
+
22
+ expect(b2.firefox?).to eq false
23
+ expect(b2.chrome?).to eq true
24
+ end
25
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'fast_browser'
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastBrowser do
4
+ it 'parses Firefox versions' do
5
+ browser = FastBrowser.new 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'
6
+
7
+ expect(browser.major_version).to eq 40
8
+ expect(browser.minor_version).to eq 1
9
+ end
10
+
11
+ it 'parses Chrome versions' do
12
+ browser = FastBrowser.new 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36'
13
+
14
+ expect(browser.major_version).to eq 41
15
+ expect(browser.minor_version).to eq 0
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ require_relative '../test_helper'
2
+
3
+ require 'benchmark'
4
+ require 'minitest/autorun'
5
+
6
+ require 'browser'
7
+ require 'ruby-prof'
8
+
9
+ class TestCompare < Minitest::Test
10
+ TIMES = 50_000
11
+
12
+ FIREFOX_40 = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1'
13
+
14
+ def test_compare_with_browser
15
+ ua = FIREFOX_40
16
+ result = nil
17
+
18
+ # Make sure the Rust lib is hot
19
+ _ = FastBrowser.new ua
20
+
21
+ test_browser = lambda do
22
+ TIMES.times do
23
+ b = Browser.new user_agent: ua
24
+ assert_equal b.firefox?, true
25
+ assert_equal "40", b.version
26
+ end
27
+ end
28
+
29
+ test_fast_browser = lambda do
30
+ TIMES.times do
31
+ b = FastBrowser.new ua
32
+ assert_equal b.firefox?, true
33
+ assert_equal 40, b.major_version
34
+ end
35
+ end
36
+
37
+ {
38
+ 'browser' => test_browser,
39
+ 'fast_browser' => test_fast_browser
40
+ }.each do |(name, test_block)|
41
+ time = benchmark_time &test_block
42
+ objects = benchmark_memory &test_block
43
+
44
+ print "#{name.ljust(15)}%10.5f seconds\t%d objects\n" % [time, objects]
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def profile(&block)
51
+ result = RubyProf.profile &block
52
+ RubyProf::FlatPrinter.new(result).print(STDOUT)
53
+ end
54
+
55
+ def benchmark_time &block
56
+ Benchmark.realtime &block
57
+ end
58
+
59
+ def benchmark_memory &block
60
+ GC.start
61
+
62
+ before_allocated = GC.stat[:total_allocated_objects]
63
+ GC.disable
64
+
65
+ block.call
66
+
67
+ after_allocated = GC.stat[:total_allocated_objects]
68
+ GC.enable
69
+
70
+ return after_allocated - before_allocated
71
+ end
72
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'fast_browser'
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_browser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dirk Gadsden
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.9.10
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.9.10
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 10.4.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 10.4.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.4.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.4.0
69
+ description: Blazing-fast, Rust-powered user agent detection library.
70
+ email:
71
+ - dirk@esherido.com
72
+ executables: []
73
+ extensions:
74
+ - ext/extconf.rb
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - Gemfile.lock
82
+ - Rakefile
83
+ - ext/extconf.rb
84
+ - fast_browser.gemspec
85
+ - lib/fast_browser.rb
86
+ - lib/fast_browser/library_extensions.rb
87
+ - lib/fast_browser/version.rb
88
+ - rust/Cargo.toml
89
+ - rust/build-debug.sh
90
+ - rust/src/bot.rs
91
+ - rust/src/browser.rs
92
+ - rust/src/lib.rs
93
+ - rust/src/user_agent.rs
94
+ - rust/src/util.rs
95
+ - spec/name_spec.rb
96
+ - spec/safety_spec.rb
97
+ - spec/spec_helper.rb
98
+ - spec/version_spec.rb
99
+ - test/benchmark/compare.rb
100
+ - test/test_helper.rb
101
+ homepage: https://github.com/dirk/fast_browser
102
+ licenses:
103
+ - ''
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.5.1
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Blazing-fast, Rust-powered user agent detection library.
125
+ test_files: []