fast_browser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []