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 +7 -0
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/.travis.yml +29 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +37 -0
- data/Rakefile +1 -0
- data/ext/extconf.rb +39 -0
- data/fast_browser.gemspec +26 -0
- data/lib/fast_browser/library_extensions.rb +15 -0
- data/lib/fast_browser/version.rb +3 -0
- data/lib/fast_browser.rb +69 -0
- data/rust/Cargo.toml +13 -0
- data/rust/build-debug.sh +17 -0
- data/rust/src/bot.rs +66 -0
- data/rust/src/browser.rs +181 -0
- data/rust/src/lib.rs +109 -0
- data/rust/src/user_agent.rs +32 -0
- data/rust/src/util.rs +10 -0
- data/spec/name_spec.rb +45 -0
- data/spec/safety_spec.rb +25 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/version_spec.rb +17 -0
- data/test/benchmark/compare.rb +72 -0
- data/test/test_helper.rb +2 -0
- metadata +125 -0
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
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
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
|
data/lib/fast_browser.rb
ADDED
@@ -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
data/rust/build-debug.sh
ADDED
@@ -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
|
+
}
|
data/rust/src/browser.rs
ADDED
@@ -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
|
data/spec/safety_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
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: []
|