Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import (
)

func main() {
// switch to use embedded regexes
// parser.ReadFile = devicedetector.EmbeddedRegexes.ReadFile

dd, err := NewDeviceDetector("regexes")
if err != nil {
log.Fatal(err)
Expand Down
144 changes: 73 additions & 71 deletions device_detector.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package devicedetector

import (
"path/filepath"
"embed"
"path"
"strings"

regexp "github.com/dlclark/regexp2"
gover "github.com/mcuadros/go-version"

. "github.com/slipros/devicedetector/parser"
"github.com/slipros/devicedetector/parser"
"github.com/slipros/devicedetector/parser/client"
"github.com/slipros/devicedetector/parser/device"
)

//go:embed regexes/*
var EmbeddedRegexes embed.FS

const UNKNOWN = "UNK"
const VERSION = `6.4.1`

Expand All @@ -29,61 +33,59 @@ var (
func fixUserAgentRegEx(regex string) string {
reg := strings.ReplaceAll(regex, `/`, `\/`)
reg = strings.ReplaceAll(reg, `++`, `+`)

return `(?:^|[^A-Z_-])(?:` + reg + `)`
}

type DeviceDetector struct {
deviceParsers []device.DeviceParser
clientParsers []client.ClientParser
botParsers []BotParser
osParsers []OsParser
vendorParser *VendorFragments
botParsers []parser.BotParser
osParsers []parser.OsParser
vendorParser *parser.VendorFragments
DiscardBotInformation bool
SkipBotDetection bool
}

func NewDeviceDetector(dir string) (*DeviceDetector, error) {
vp, err := NewVendor(filepath.Join(dir, FixtureFileVendor))
vp, err := parser.NewVendor(path.Join(dir, parser.FixtureFileVendor))
if err != nil {
return nil, err
}
osp, err := NewOss(filepath.Join(dir, FixtureFileOs))
osp, err := parser.NewOss(path.Join(dir, parser.FixtureFileOs))
if err != nil {
return nil, err
}

d := &DeviceDetector{
return &DeviceDetector{
vendorParser: vp,
osParsers: []OsParser{osp},
}

clientDir := filepath.Join(dir, "client")
d.clientParsers = client.NewClientParsers(clientDir,
[]string{
client.ParserNameFeedReader,
client.ParserNameMobileApp,
client.ParserNameMediaPlayer,
client.ParserNamePim,
client.ParserNameBrowser,
client.ParserNameLibrary,
})

deviceDir := filepath.Join(dir, "device")
d.deviceParsers = device.NewDeviceParsers(deviceDir,
[]string{
device.ParserNameHbbTv,
device.ParserNameConsole,
device.ParserNameCar,
device.ParserNameCamera,
device.ParserNamePortableMediaPlayer,
device.ParserNameMobile,
})

d.botParsers = []BotParser{
NewBot(filepath.Join(dir, FixtureFileBot)),
}

return d, nil
osParsers: []parser.OsParser{osp},
clientParsers: client.NewClientParsers(
path.Join(dir, "client"),
[]string{
client.ParserNameFeedReader,
client.ParserNameMobileApp,
client.ParserNameMediaPlayer,
client.ParserNamePim,
client.ParserNameBrowser,
client.ParserNameLibrary,
},
),
deviceParsers: device.NewDeviceParsers(
path.Join(dir, "device"),
[]string{
device.ParserNameHbbTv,
device.ParserNameConsole,
device.ParserNameCar,
device.ParserNameCamera,
device.ParserNamePortableMediaPlayer,
device.ParserNameMobile,
},
),
botParsers: []parser.BotParser{
parser.NewBot(path.Join(dir, parser.FixtureFileBot)),
},
}, nil
}

func (d *DeviceDetector) AddClientParser(cp client.ClientParser) {
Expand All @@ -102,27 +104,27 @@ func (d *DeviceDetector) GetDeviceParsers() []device.DeviceParser {
return d.deviceParsers
}

func (d *DeviceDetector) AddBotParser(op BotParser) {
func (d *DeviceDetector) AddBotParser(op parser.BotParser) {
d.botParsers = append(d.botParsers, op)
}

func (d *DeviceDetector) GetBotParsers() []BotParser {
func (d *DeviceDetector) GetBotParsers() []parser.BotParser {
return d.botParsers
}

func (d *DeviceDetector) ParseBot(ua string) *BotMatchResult {
func (d *DeviceDetector) ParseBot(ua string) *parser.BotMatchResult {
if !d.SkipBotDetection {
for _, parser := range d.botParsers {
parser.DiscardDetails(d.DiscardBotInformation)
if r := parser.Parse(ua); r != nil {
for _, p := range d.botParsers {
p.DiscardDetails(d.DiscardBotInformation)
if r := p.Parse(ua); r != nil {
return r
}
}
}
return nil
}

func (d *DeviceDetector) ParseOs(ua string) *OsMatchResult {
func (d *DeviceDetector) ParseOs(ua string) *parser.OsMatchResult {
for _, p := range d.osParsers {
if r := p.Parse(ua); r != nil {
return r
Expand All @@ -132,17 +134,17 @@ func (d *DeviceDetector) ParseOs(ua string) *OsMatchResult {
}

func (d *DeviceDetector) ParseClient(ua string) *client.ClientMatchResult {
for _, parser := range d.clientParsers {
if r := parser.Parse(ua); r != nil {
for _, p := range d.clientParsers {
if r := p.Parse(ua); r != nil {
return r
}
}
return nil
}

func (d *DeviceDetector) ParseDevice(ua string) *device.DeviceMatchResult {
for _, parser := range d.deviceParsers {
if r := parser.Parse(ua); r != nil {
for _, p := range d.deviceParsers {
if r := p.Parse(ua); r != nil {
return r
}
}
Expand All @@ -163,80 +165,80 @@ func (d *DeviceDetector) parseInfo(info *DeviceInfo) {

os := info.GetOs()
osShortName := os.ShortName
osFamily := GetOsFamily(osShortName)
osFamily := parser.GetOsFamily(osShortName)
osVersion := os.Version
cmr := info.GetClient()

if info.Brand == "" && (osShortName == `ATV` || osShortName == `IOS` || osShortName == `MAC`) {
info.Brand = `AP`
}

deviceType := GetDeviceType(info.Type)
deviceType := parser.GetDeviceType(info.Type)
// Chrome on Android passes the device type based on the keyword 'Mobile'
// If it is present the device should be a smartphone, otherwise it's a tablet
// See https://developer.chrome.com/multidevice/user-agent#chrome_for_android_user_agent
if deviceType == DEVICE_TYPE_INVALID && osFamily == `Android` {
if deviceType == parser.DEVICE_TYPE_INVALID && osFamily == `Android` {
if browserName, ok := client.GetBrowserFamily(cmr.ShortName); ok && browserName == `Chrome` {
if ok, _ := chrMobReg.MatchString(ua); ok {
deviceType = DEVICE_TYPE_SMARTPHONE
deviceType = parser.DEVICE_TYPE_SMARTPHONE
} else if ok, _ = chrTabReg.MatchString(ua); ok {
deviceType = DEVICE_TYPE_TABLET
deviceType = parser.DEVICE_TYPE_TABLET
}
}
}

if deviceType == DEVICE_TYPE_INVALID {
if deviceType == parser.DEVICE_TYPE_INVALID {
if info.HasAndroidMobileFragment() {
deviceType = DEVICE_TYPE_TABLET
deviceType = parser.DEVICE_TYPE_TABLET
} else if ok, _ := opaTabReg.MatchString(ua); ok {
deviceType = DEVICE_TYPE_TABLET
deviceType = parser.DEVICE_TYPE_TABLET
} else if info.HasAndroidMobileFragment() {
deviceType = DEVICE_TYPE_SMARTPHONE
deviceType = parser.DEVICE_TYPE_SMARTPHONE
} else if osShortName == "AND" && osVersion != "" {
if gover.CompareSimple(osVersion, `2.0`) == -1 {
deviceType = DEVICE_TYPE_SMARTPHONE
deviceType = parser.DEVICE_TYPE_SMARTPHONE
} else if gover.CompareSimple(osVersion, `3.0`) >= 0 &&
gover.CompareSimple(osVersion, `4.0`) == -1 {
deviceType = DEVICE_TYPE_TABLET
deviceType = parser.DEVICE_TYPE_TABLET
}
}
}

// All detected feature phones running android are more likely a smartphone
if deviceType == DEVICE_TYPE_FEATURE_PHONE && osFamily == `Android` {
deviceType = DEVICE_TYPE_SMARTPHONE
if deviceType == parser.DEVICE_TYPE_FEATURE_PHONE && osFamily == `Android` {
deviceType = parser.DEVICE_TYPE_SMARTPHONE
}

// According to http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx
if deviceType == DEVICE_TYPE_INVALID &&
if deviceType == parser.DEVICE_TYPE_INVALID &&
(osShortName == `WRT` || (osShortName == `WIN` && gover.CompareSimple(osVersion, `8`) >= 0)) &&
info.IsTouchEnabled() {
deviceType = DEVICE_TYPE_TABLET
deviceType = parser.DEVICE_TYPE_TABLET
}

// All devices running Opera TV Store are assumed to be a tv
if ok, _ := opaTvReg.MatchString(ua); ok {
deviceType = DEVICE_TYPE_TV
deviceType = parser.DEVICE_TYPE_TV
}

// Devices running Kylo or Espital TV Browsers are assumed to be a TV
if deviceType == DEVICE_TYPE_INVALID {
if deviceType == parser.DEVICE_TYPE_INVALID {
if cmr.Name == `Kylo` || cmr.Name == `Espial TV Browser` {
deviceType = DEVICE_TYPE_TV
deviceType = parser.DEVICE_TYPE_TV
} else if info.IsDesktop() {
deviceType = DEVICE_TYPE_DESKTOP
deviceType = parser.DEVICE_TYPE_DESKTOP
}
}

if deviceType != DEVICE_TYPE_INVALID {
info.Type = GetDeviceName(deviceType)
if deviceType != parser.DEVICE_TYPE_INVALID {
info.Type = parser.GetDeviceName(deviceType)
}
return
}

func (d *DeviceDetector) Parse(ua string) *DeviceInfo {
// skip parsing for empty useragents or those not containing any letter
if !StringContainsLetter(ua) {
if !parser.StringContainsLetter(ua) {
return nil
}

Expand Down
16 changes: 8 additions & 8 deletions device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ func TestParseInvalidUA(t *testing.T) {

func TestInstanceReusage(t *testing.T) {
userAgents := [][]string{
[]string{
{
`Sraf/3.0 (Linux i686 ; U; HbbTV/1.1.1 (+PVR+DL;NEXtUS; TV44; sw1.0) CE-HTML/1.0 Config(L:eng,CC:DEU); en/de)`,
``,
``,
},
[]string{
{
`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`,
`Archos`,
`101 PLATINUM`,
},
[]string{
{
`Opera/9.80 (Linux mips; U; HbbTV/1.1.1 (; Vestel; MB95; 1.0; 1.0; ); en) Presto/2.10.287 Version/12.00`,
`Vestel`,
`MB95`,
Expand All @@ -50,11 +50,11 @@ func TestInstanceReusage(t *testing.T) {

func TestVersionTruncation(t *testing.T) {
data := map[int][]string{
VERSION_TRUNCATION_NONE: []string{`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2.2`, `34.0.1847.114`},
VERSION_TRUNCATION_BUILD: []string{`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2.2`, `34.0.1847.114`},
VERSION_TRUNCATION_PATCH: []string{`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2.2`, `34.0.1847`},
VERSION_TRUNCATION_MINOR: []string{`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2`, `34.0`},
VERSION_TRUNCATION_MAJOR: []string{`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4`, `34`},
VERSION_TRUNCATION_NONE: {`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2.2`, `34.0.1847.114`},
VERSION_TRUNCATION_BUILD: {`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2.2`, `34.0.1847.114`},
VERSION_TRUNCATION_PATCH: {`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2.2`, `34.0.1847`},
VERSION_TRUNCATION_MINOR: {`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4.2`, `34.0`},
VERSION_TRUNCATION_MAJOR: {`Mozilla/5.0 (Linux; Android 4.2.2; ARCHOS 101 PLATINUM Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Safari/537.36`, `4`, `34`},
}
for k, v := range data {
SetVersionTruncation(k)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/slipros/devicedetector

go 1.13
go 1.23

require (
github.com/dlclark/regexp2 v1.11.4
Expand Down
6 changes: 4 additions & 2 deletions parser/bot.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package parser

import "path/filepath"
import (
"path"
)

var botFactory = make(map[string]func(string) BotParser)

Expand All @@ -26,7 +28,7 @@ const FixtureFileBot = `bots.yml`
func init() {
RegBotParser(ParserNameBot,
func(dir string) BotParser {
return NewBot(filepath.Join(dir, FixtureFileBot))
return NewBot(path.Join(dir, FixtureFileBot))
})
}

Expand Down
7 changes: 4 additions & 3 deletions parser/bot_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package parser

import (
"gotest.tools/assert"
"path/filepath"
"path"
"testing"

"gotest.tools/assert"
)

var botParser = NewBot(filepath.Join(dir, FixtureFileBot))
var botParser = NewBot(path.Join(dir, FixtureFileBot))

func TestGetInfoFromUABot(t *testing.T) {
ua := `Googlebot/2.1 (http://www.googlebot.com/bot.html)`
Expand Down
4 changes: 2 additions & 2 deletions parser/client/browser.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package client

import (
"path/filepath"
"path"

gover "github.com/mcuadros/go-version"

Expand Down Expand Up @@ -838,7 +838,7 @@ const FixtureFileBrowser = `browsers.yml`
func init() {
RegClientParser(ParserNameBrowser,
func(dir string) ClientParser {
return NewBrowser(filepath.Join(dir, FixtureFileBrowser))
return NewBrowser(path.Join(dir, FixtureFileBrowser))
})
}

Expand Down
Loading