From c88c537b7dfa315762dcffe4f6c9da1889b21621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Thu, 14 Aug 2025 07:47:21 +0200 Subject: [PATCH 01/20] feat: NewFromSavedWithOptions --- uaparser/parser.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uaparser/parser.go b/uaparser/parser.go index 9d617f6..2a78a1d 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -234,6 +234,10 @@ func New(regexFile string) (*Parser, error) { } func NewFromSaved() *Parser { + return NewFromSavedWithOptions(defaultParserConfig()) +} + +func NewFromSavedWithOptions(config *parserConfig) *Parser { parser, err := newFromBytes(DefinitionYaml, defaultParserConfig()) if err != nil { // if the YAML is malformed, it's a programmatic error inside what From 79df3d88a2e80b36aa55e7582048aa94b74a42ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Thu, 21 Aug 2025 08:59:45 +0200 Subject: [PATCH 02/20] Update uaparser/parser.go Co-authored-by: David Goldstein --- uaparser/parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaparser/parser.go b/uaparser/parser.go index 2a78a1d..a50c869 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -238,7 +238,7 @@ func NewFromSaved() *Parser { } func NewFromSavedWithOptions(config *parserConfig) *Parser { - parser, err := newFromBytes(DefinitionYaml, defaultParserConfig()) + parser, err := newFromBytes(DefinitionYaml, config) if err != nil { // if the YAML is malformed, it's a programmatic error inside what // we've statically-compiled in our binary. Panic! From ce533867939055ee7e031f42ee8eebce48b3e870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Thu, 18 Sep 2025 22:37:01 +0200 Subject: [PATCH 03/20] Introduce New function with functional options pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- README.md | 9 ++- test.go | 29 ++++++-- uaparser/benchmark_test.go | 41 ++++++++--- uaparser/options.go | 39 ++++++++++ uaparser/parser.go | 143 +++++++++++++------------------------ uaparser/parsing_test.go | 17 ++++- 6 files changed, 167 insertions(+), 111 deletions(-) create mode 100644 uaparser/options.go diff --git a/README.md b/README.md index b19e29c..e37bd83 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ package main import ( "fmt" "log" + "os" "github.com/ua-parser/uap-go/uaparser" ) @@ -49,7 +50,13 @@ import ( func main() { uagent := "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.1.0-80) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true" - parser, err := uaparser.New("./regexes.yaml") + + regexes, err := os.ReadFile("./regexes.yaml") + if err != nil { + log.Fatal(err) + } + + parser, err := uaparser.New(regexes) if err != nil { log.Fatal(err) } diff --git a/test.go b/test.go index 0b06021..42f4f69 100644 --- a/test.go +++ b/test.go @@ -16,12 +16,26 @@ func main() { fmt.Printf("Usage: %s [old|new|both] [concurrency level]\n", os.Args[0]) return } + + regexes, err := os.ReadFile("./uap-core/regexes.yaml") + if err != nil { + fmt.Printf("Failed to read regexes file. Error: %s\n", err.Error()) + return + } + var wg sync.WaitGroup cLevel, _ := strconv.Atoi(os.Args[2]) switch os.Args[1] { case "new": fmt.Println("Running new version of uap...") - uaParser, _ := uaparser.NewWithOptions("./uap-core/regexes.yaml", (uaparser.EOsLookUpMode | uaparser.EUserAgentLookUpMode), 100, 20, true, true, 1024) + uaParser, _ := uaparser.New(regexes, + uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), + uaparser.WithMissesThreshold(100), + uaparser.WithMatchIdxNotOk(20), + uaparser.WithSort(true), + uaparser.WithDebug(true), + uaparser.WithCacheSize(1024), + ) for i := 0; i < cLevel; i++ { wg.Add(1) go runTest(uaParser, i, &wg) @@ -30,7 +44,7 @@ func main() { return case "old": fmt.Println("Running old version of uap...") - uaParser, _ := uaparser.New("./uap-core/regexes.yaml") + uaParser, _ := uaparser.New(regexes) for i := 0; i < cLevel; i++ { wg.Add(1) go runTest(uaParser, i, &wg) @@ -39,13 +53,20 @@ func main() { return case "both": fmt.Println("Running new version of uap...") - uaParser, _ := uaparser.NewWithOptions("./uap-core/regexes.yaml", (uaparser.EOsLookUpMode | uaparser.EUserAgentLookUpMode), 100, 20, true, true, 1024) + uaParser, _ := uaparser.New(regexes, + uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), + uaparser.WithMissesThreshold(100), + uaparser.WithMatchIdxNotOk(20), + uaparser.WithSort(true), + uaparser.WithDebug(true), + uaparser.WithCacheSize(1024), + ) for i := 0; i < cLevel; i++ { wg.Add(1) runTest(uaParser, i, &wg) } fmt.Println("Running old version of uap...") - uaParser, _ = uaparser.New("./uap-core/regexes.yaml") + uaParser, _ = uaparser.New(regexes) for i := 0; i < cLevel; i++ { wg.Add(1) runTest(uaParser, i, &wg) diff --git a/uaparser/benchmark_test.go b/uaparser/benchmark_test.go index 1fc99c1..2e07f54 100644 --- a/uaparser/benchmark_test.go +++ b/uaparser/benchmark_test.go @@ -15,11 +15,25 @@ var largeUasSample []string func init() { var err error - benchedParser, err = New("../uap-core/regexes.yaml") + + regexes, err := os.ReadFile("../uap-core/regexes.yaml") + if err != nil { + log.Fatal(err) + } + + benchedParser, err = New(regexes) if err != nil { log.Fatal(err) } - benchedParserWithOptions, err = NewWithOptions("../uap-core/regexes.yaml", (EOsLookUpMode | EUserAgentLookUpMode), 100, 20, true, true, cDefaultCacheSize) + benchedParserWithOptions, err = New(regexes, + WithMode(EOsLookUpMode|EUserAgentLookUpMode), + WithMissesThreshold(100), + WithMatchIdxNotOk(20), + WithSort(true), + WithDebug(true), + WithCacheSize(cDefaultCacheSize), + ) + if err != nil { log.Fatal(err) } @@ -48,17 +62,22 @@ func BenchmarkParserWithOptions(b *testing.B) { } func BenchmarkParserWithDifferentCacheSize(b *testing.B) { - sizes := []int{cDefaultCacheSize, cDefaultCacheSize*2, cDefaultCacheSize*3, cDefaultCacheSize*4} + sizes := []int{cDefaultCacheSize, cDefaultCacheSize * 2, cDefaultCacheSize * 3, cDefaultCacheSize * 4} + + regexes, err := os.ReadFile("../uap-core/regexes.yaml") + if err != nil { + log.Fatal(err) + } for _, size := range sizes { - parser, err := NewWithOptions( - "../uap-core/regexes.yaml", - EOsLookUpMode | EUserAgentLookUpMode | EDeviceLookUpMode, - cDefaultMissesTreshold, - cDefaultMatchIdxNotOk, - false, - false, - size) + parser, err := New(regexes, + WithMode(EOsLookUpMode|EUserAgentLookUpMode|EDeviceLookUpMode), + WithMissesThreshold(cDefaultMissesTreshold), + WithMatchIdxNotOk(cDefaultMatchIdxNotOk), + WithSort(false), + WithDebug(false), + WithCacheSize(size), + ) if err != nil { log.Fatal(err) } diff --git a/uaparser/options.go b/uaparser/options.go new file mode 100644 index 0000000..1f6334d --- /dev/null +++ b/uaparser/options.go @@ -0,0 +1,39 @@ +package uaparser + +type Option func(*Parser) + +func WithMode(mode LookupMode) Option { + return func(s *Parser) { + s.config.Mode = mode + } +} + +func WithSort(useSort bool) Option { + return func(s *Parser) { + s.config.UseSort = useSort + } +} + +func WithDebug(debug bool) Option { + return func(s *Parser) { + s.config.DebugMode = debug + } +} + +func WithCacheSize(size int) Option { + return func(s *Parser) { + s.config.CacheSize = size + } +} + +func WithMissesThreshold(threshold uint64) Option { + return func(s *Parser) { + s.config.MissesThreshold = threshold + } +} + +func WithMatchIdxNotOk(idx int) Option { + return func(s *Parser) { + s.config.MatchIdxNotOk = idx + } +} diff --git a/uaparser/parser.go b/uaparser/parser.go index a50c869..7b3b0d2 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -2,7 +2,6 @@ package uaparser import ( "fmt" - "os" "regexp" "sort" "sync" @@ -129,7 +128,7 @@ type Client struct { } type parserConfig struct { - Mode int + Mode LookupMode UseSort bool DebugMode bool CacheSize int @@ -146,118 +145,63 @@ type Parser struct { DeviceMisses uint64 config *parserConfig - cache *cache + cache *cache RegexesDefinitions } +type LookupMode int + const ( - EOsLookUpMode = 1 /* 00000001 */ - EUserAgentLookUpMode = 2 /* 00000010 */ - EDeviceLookUpMode = 4 /* 00000100 */ - cMinMissesTreshold = 100000 - cDefaultMissesTreshold = 500000 - cDefaultMatchIdxNotOk = 20 - cDefaultSortOption = false - cDefaultDebugMode = false - cDefaultCacheSize = 1024 + EOsLookUpMode LookupMode = 1 /* 00000001 */ + EUserAgentLookUpMode LookupMode = 2 /* 00000010 */ + EDeviceLookUpMode LookupMode = 4 /* 00000100 */ + cMinMissesTreshold = 100000 + cDefaultMissesTreshold = 500000 + cDefaultMatchIdxNotOk = 20 + cDefaultSortOption = false + cDefaultDebugMode = false + cDefaultCacheSize = 1024 ) -func (parser *Parser) mustCompile() { // until we can use yaml.UnmarshalYAML with embedded pointer struct - for _, p := range parser.UA { - p.Reg = compileRegex(p.Flags, p.Expr) - p.setDefaults() - } - for _, p := range parser.OS { - p.Reg = compileRegex(p.Flags, p.Expr) - p.setDefaults() - } - for _, p := range parser.Device { - p.Reg = compileRegex(p.Flags, p.Expr) - p.setDefaults() +func New(data []byte, options ...Option) (*Parser, error) { + parser := &Parser{ + config: &parserConfig{ + Mode: EOsLookUpMode | EUserAgentLookUpMode | EDeviceLookUpMode, + UseSort: cDefaultSortOption, + DebugMode: cDefaultDebugMode, + CacheSize: cDefaultCacheSize, + MissesThreshold: cMinMissesTreshold, + MatchIdxNotOk: cDefaultMatchIdxNotOk, + }, } -} -func defaultParserConfig() *parserConfig { - return &parserConfig{ - Mode: EOsLookUpMode | EUserAgentLookUpMode | EDeviceLookUpMode, - UseSort: cDefaultSortOption, - DebugMode: cDefaultDebugMode, - CacheSize: cDefaultCacheSize, - MissesThreshold: cMinMissesTreshold, - MatchIdxNotOk: cDefaultMatchIdxNotOk, + for _, o := range options { + o(parser) } -} -func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debugMode bool, cacheSize int) (*Parser, error) { - data, err := os.ReadFile(regexFile) - if nil != err { - return nil, err + if parser.config.MatchIdxNotOk < 0 { + parser.config.MatchIdxNotOk = 0 } - cfg := &parserConfig{ - Mode: mode, - UseSort: useSort, - DebugMode: debugMode, - MatchIdxNotOk: cDefaultMatchIdxNotOk, - MissesThreshold: cDefaultMissesTreshold, - CacheSize: cDefaultCacheSize, + if parser.config.MissesThreshold <= cMinMissesTreshold { + parser.config.MissesThreshold = cMinMissesTreshold } - if topCnt >= 0 { - cfg.MatchIdxNotOk = topCnt - } - if treshold > cMinMissesTreshold { - cfg.MissesThreshold = uint64(treshold) - } - if cacheSize > 0 { - cfg.CacheSize = cacheSize + if parser.config.CacheSize < 0 { + parser.config.CacheSize = cDefaultCacheSize } - parser, err := newFromBytes(data, cfg) - if err != nil { - return nil, err + if parser.cache == nil { + parser.cache = newCache(parser.config.CacheSize) } - return parser, nil -} -func New(regexFile string) (*Parser, error) { - data, err := os.ReadFile(regexFile) - if nil != err { - return nil, err - } - parser, err := newFromBytes(data, defaultParserConfig()) - if err != nil { - return nil, err + if data == nil { + data = DefinitionYaml } - return parser, nil -} -func NewFromSaved() *Parser { - return NewFromSavedWithOptions(defaultParserConfig()) -} - -func NewFromSavedWithOptions(config *parserConfig) *Parser { - parser, err := newFromBytes(DefinitionYaml, config) - if err != nil { - // if the YAML is malformed, it's a programmatic error inside what - // we've statically-compiled in our binary. Panic! - panic(err.Error()) - } - return parser -} - -func NewFromBytes(data []byte) (*Parser, error) { - return newFromBytes(data, defaultParserConfig()) -} - -func newFromBytes(data []byte, config *parserConfig) (*Parser, error) { - parser := &Parser{ - config: config, - cache: newCache(config.CacheSize), - } if err := yaml.Unmarshal(data, &parser.RegexesDefinitions); err != nil { - return nil, err + return nil, fmt.Errorf("error parsing regexes definitions: %w", err) } parser.mustCompile() @@ -265,6 +209,21 @@ func newFromBytes(data []byte, config *parserConfig) (*Parser, error) { return parser, nil } +func (parser *Parser) mustCompile() { // until we can use yaml.UnmarshalYAML with embedded pointer struct + for _, p := range parser.UA { + p.Reg = compileRegex(p.Flags, p.Expr) + p.setDefaults() + } + for _, p := range parser.OS { + p.Reg = compileRegex(p.Flags, p.Expr) + p.setDefaults() + } + for _, p := range parser.Device { + p.Reg = compileRegex(p.Flags, p.Expr) + p.setDefaults() + } +} + func (parser *Parser) Parse(line string) *Client { cli := new(Client) var wg sync.WaitGroup diff --git a/uaparser/parsing_test.go b/uaparser/parsing_test.go index a7a303b..c58280c 100644 --- a/uaparser/parsing_test.go +++ b/uaparser/parsing_test.go @@ -12,7 +12,13 @@ var testParser *Parser func init() { var err error - testParser, err = New("../uap-core/regexes.yaml") + + uaRegexes, err := os.ReadFile("../uap-core/regexes.yaml") + if err != nil { + log.Fatal(err) + } + + testParser, err = New(uaRegexes) if err != nil { log.Fatal(err) } @@ -38,8 +44,13 @@ func TestOSParsing(t *testing.T) { } } -func TestReadsInteralYAML(t *testing.T) { - _ = NewFromSaved() // should not panic +func TestReadsInternalYAML(t *testing.T) { + t.Parallel() + + _, err := New(DefinitionYaml) + if err != nil { + t.Fatal(err) + } } func TestUAParsing(t *testing.T) { From 9dccb5184d5745a3a91e0470ce462db0c8764d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Thu, 16 Oct 2025 09:28:39 +0200 Subject: [PATCH 04/20] migrate data to functional option as well. --- README.md | 10 ++++++++- test.go | 18 ++++++++++++---- uaparser/options.go | 6 ++++++ uaparser/parser.go | 50 +++++++++++++++++++++++++-------------------- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index e37bd83..851a10a 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ import ( "os" "github.com/ua-parser/uap-go/uaparser" + "gopkg.in/yaml.v3" ) func main() { @@ -55,8 +56,15 @@ func main() { if err != nil { log.Fatal(err) } + + var def *uaparser.RegexesDefinitions + + if err := yaml.Unmarshal(regexes, def); err != nil { + fmt.Printf("error parsing regexes definitions. Error: %s\n", err.Error()) + return + } - parser, err := uaparser.New(regexes) + parser, err := uaparser.New(uaparser.WithRegexesDefinitions(def)) if err != nil { log.Fatal(err) } diff --git a/test.go b/test.go index 42f4f69..7beb8c4 100644 --- a/test.go +++ b/test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/ua-parser/uap-go/uaparser" + "gopkg.in/yaml.v3" ) func main() { @@ -23,12 +24,20 @@ func main() { return } + var def *uaparser.RegexesDefinitions + + if err := yaml.Unmarshal(regexes, def); err != nil { + fmt.Printf("error parsing regexes definitions. Error: %s\n", err.Error()) + return + } + var wg sync.WaitGroup cLevel, _ := strconv.Atoi(os.Args[2]) switch os.Args[1] { case "new": fmt.Println("Running new version of uap...") - uaParser, _ := uaparser.New(regexes, + uaParser, _ := uaparser.New( + uaparser.WithRegexesDefinitions(def), uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), uaparser.WithMissesThreshold(100), uaparser.WithMatchIdxNotOk(20), @@ -44,7 +53,7 @@ func main() { return case "old": fmt.Println("Running old version of uap...") - uaParser, _ := uaparser.New(regexes) + uaParser, _ := uaparser.New(uaparser.WithRegexesDefinitions(def)) for i := 0; i < cLevel; i++ { wg.Add(1) go runTest(uaParser, i, &wg) @@ -53,7 +62,8 @@ func main() { return case "both": fmt.Println("Running new version of uap...") - uaParser, _ := uaparser.New(regexes, + uaParser, _ := uaparser.New( + uaparser.WithRegexesDefinitions(def), uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), uaparser.WithMissesThreshold(100), uaparser.WithMatchIdxNotOk(20), @@ -66,7 +76,7 @@ func main() { runTest(uaParser, i, &wg) } fmt.Println("Running old version of uap...") - uaParser, _ = uaparser.New(regexes) + uaParser, _ = uaparser.New(uaparser.WithRegexesDefinitions(def)) for i := 0; i < cLevel; i++ { wg.Add(1) runTest(uaParser, i, &wg) diff --git a/uaparser/options.go b/uaparser/options.go index 1f6334d..b595daf 100644 --- a/uaparser/options.go +++ b/uaparser/options.go @@ -37,3 +37,9 @@ func WithMatchIdxNotOk(idx int) Option { s.config.MatchIdxNotOk = idx } } + +func WithRegexesDefinitions(def *RegexesDefinitions) Option { + return func(s *Parser) { + s.RegexesDefinitions = def + } +} diff --git a/uaparser/parser.go b/uaparser/parser.go index 7b3b0d2..cebd284 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -11,12 +11,19 @@ import ( "gopkg.in/yaml.v3" ) +var defaultRegexesDefinitions = sync.OnceValue(func() *RegexesDefinitions { + var def *RegexesDefinitions + if err := yaml.Unmarshal(DefinitionYaml, def); err != nil { + panic(fmt.Errorf("error parsing regexes definitions: %w", err)) + } + + return def +}) + type RegexesDefinitions struct { UA []*uaParser `yaml:"user_agent_parsers"` OS []*osParser `yaml:"os_parsers"` Device []*deviceParser `yaml:"device_parsers"` - _ [4]byte // padding for alignment - sync.RWMutex } type UserAgentSorter []*uaParser @@ -147,7 +154,9 @@ type Parser struct { config *parserConfig cache *cache - RegexesDefinitions + *RegexesDefinitions + + mu *sync.RWMutex } type LookupMode int @@ -164,7 +173,7 @@ const ( cDefaultCacheSize = 1024 ) -func New(data []byte, options ...Option) (*Parser, error) { +func New(options ...Option) (*Parser, error) { parser := &Parser{ config: &parserConfig{ Mode: EOsLookUpMode | EUserAgentLookUpMode | EDeviceLookUpMode, @@ -174,6 +183,7 @@ func New(data []byte, options ...Option) (*Parser, error) { MissesThreshold: cMinMissesTreshold, MatchIdxNotOk: cDefaultMatchIdxNotOk, }, + mu: &sync.RWMutex{}, } for _, o := range options { @@ -196,12 +206,8 @@ func New(data []byte, options ...Option) (*Parser, error) { parser.cache = newCache(parser.config.CacheSize) } - if data == nil { - data = DefinitionYaml - } - - if err := yaml.Unmarshal(data, &parser.RegexesDefinitions); err != nil { - return nil, fmt.Errorf("error parsing regexes definitions: %w", err) + if parser.RegexesDefinitions == nil { + parser.RegexesDefinitions = defaultRegexesDefinitions() } parser.mustCompile() @@ -231,27 +237,27 @@ func (parser *Parser) Parse(line string) *Client { wg.Add(1) go func() { defer wg.Done() - parser.RLock() + parser.mu.RLock() cli.UserAgent = parser.ParseUserAgent(line) - parser.RUnlock() + parser.mu.RUnlock() }() } if EOsLookUpMode&parser.config.Mode == EOsLookUpMode { wg.Add(1) go func() { defer wg.Done() - parser.RLock() + parser.mu.RLock() cli.Os = parser.ParseOs(line) - parser.RUnlock() + parser.mu.RUnlock() }() } if EDeviceLookUpMode&parser.config.Mode == EDeviceLookUpMode { wg.Add(1) go func() { defer wg.Done() - parser.RLock() + parser.mu.RLock() cli.Device = parser.ParseDevice(line) - parser.RUnlock() + parser.mu.RUnlock() }() } wg.Wait() @@ -347,7 +353,7 @@ func (parser *Parser) ParseDevice(line string) *Device { } func checkAndSort(parser *Parser) { - parser.Lock() + parser.mu.Lock() if atomic.LoadUint64(&parser.UserAgentMisses) >= parser.config.MissesThreshold { if parser.config.DebugMode { fmt.Printf("%s\tSorting UserAgents slice\n", time.Now()) @@ -355,8 +361,8 @@ func checkAndSort(parser *Parser) { parser.UserAgentMisses = 0 sort.Sort(UserAgentSorter(parser.UA)) } - parser.Unlock() - parser.Lock() + parser.mu.Unlock() + parser.mu.Lock() if atomic.LoadUint64(&parser.OsMisses) >= parser.config.MissesThreshold { if parser.config.DebugMode { fmt.Printf("%s\tSorting OS slice\n", time.Now()) @@ -364,8 +370,8 @@ func checkAndSort(parser *Parser) { parser.OsMisses = 0 sort.Sort(OsSorter(parser.OS)) } - parser.Unlock() - parser.Lock() + parser.mu.Unlock() + parser.mu.Lock() if atomic.LoadUint64(&parser.DeviceMisses) >= parser.config.MissesThreshold { if parser.config.DebugMode { fmt.Printf("%s\tSorting Device slice\n", time.Now()) @@ -373,7 +379,7 @@ func checkAndSort(parser *Parser) { parser.DeviceMisses = 0 sort.Sort(DeviceSorter(parser.Device)) } - parser.Unlock() + parser.mu.Unlock() } func compileRegex(flags, expr string) *regexp.Regexp { From ea8efb5229b6683964b6ad6199908bbd83ea5143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 00:18:25 +0200 Subject: [PATCH 05/20] fix tests --- uaparser/benchmark_test.go | 20 +++++++++++++++++--- uaparser/parsing_test.go | 10 ++++++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/uaparser/benchmark_test.go b/uaparser/benchmark_test.go index 2e07f54..e46b987 100644 --- a/uaparser/benchmark_test.go +++ b/uaparser/benchmark_test.go @@ -7,6 +7,8 @@ import ( "os" "strings" "testing" + + "gopkg.in/yaml.v3" ) var benchedParser *Parser @@ -21,11 +23,17 @@ func init() { log.Fatal(err) } - benchedParser, err = New(regexes) + var def *RegexesDefinitions + + if err := yaml.Unmarshal(regexes, def); err != nil { + log.Fatal(err) + } + + benchedParser, err = New(WithRegexesDefinitions(def)) if err != nil { log.Fatal(err) } - benchedParserWithOptions, err = New(regexes, + benchedParserWithOptions, err = New(WithRegexesDefinitions(def), WithMode(EOsLookUpMode|EUserAgentLookUpMode), WithMissesThreshold(100), WithMatchIdxNotOk(20), @@ -69,8 +77,14 @@ func BenchmarkParserWithDifferentCacheSize(b *testing.B) { log.Fatal(err) } + var def *RegexesDefinitions + + if err := yaml.Unmarshal(regexes, def); err != nil { + log.Fatal(err) + } + for _, size := range sizes { - parser, err := New(regexes, + parser, err := New(WithRegexesDefinitions(def), WithMode(EOsLookUpMode|EUserAgentLookUpMode|EDeviceLookUpMode), WithMissesThreshold(cDefaultMissesTreshold), WithMatchIdxNotOk(cDefaultMatchIdxNotOk), diff --git a/uaparser/parsing_test.go b/uaparser/parsing_test.go index c58280c..1064a12 100644 --- a/uaparser/parsing_test.go +++ b/uaparser/parsing_test.go @@ -18,7 +18,13 @@ func init() { log.Fatal(err) } - testParser, err = New(uaRegexes) + var def *RegexesDefinitions + + if err := yaml.Unmarshal(uaRegexes, def); err != nil { + log.Fatal(err) + } + + testParser, err = New(WithRegexesDefinitions(def)) if err != nil { log.Fatal(err) } @@ -47,7 +53,7 @@ func TestOSParsing(t *testing.T) { func TestReadsInternalYAML(t *testing.T) { t.Parallel() - _, err := New(DefinitionYaml) + _, err := New() if err != nil { t.Fatal(err) } From 3700e7735f253fc67b65c9829ba4371224ece963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 00:28:18 +0200 Subject: [PATCH 06/20] add old functions back --- uaparser/deprecated.go | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 uaparser/deprecated.go diff --git a/uaparser/deprecated.go b/uaparser/deprecated.go new file mode 100644 index 0000000..56c8ad0 --- /dev/null +++ b/uaparser/deprecated.go @@ -0,0 +1,57 @@ +package uaparser + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +// NewWithOptions is deprecated. +// Deprecated: Use New and option functions instead. +func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debugMode bool, cacheSize int) (*Parser, error) { + uaRegexes, err := os.ReadFile(regexFile) + if err != nil { + return nil, err + } + + var def *RegexesDefinitions + + if err := yaml.Unmarshal(uaRegexes, def); err != nil { + return nil, err + } + + return New( + WithRegexesDefinitions(def), + WithMode(LookupMode(mode)), + WithMissesThreshold(uint64(treshold)), + WithMatchIdxNotOk(topCnt), + WithSort(useSort), + WithDebug(debugMode), + WithCacheSize(cacheSize), + ) +} + +// NewFromSaved is deprecated. +// Deprecated: Use New and option functions instead. +func NewFromSaved() *Parser { + parser, err := New() + if err != nil { + // if the YAML is malformed, it's a programmatic error inside what + // we've statically-compiled in our binary. Panic! + panic(err.Error()) + } + + return parser +} + +// NewFromBytes is deprecated. +// Deprecated: Use New and option functions instead. +func NewFromBytes(data []byte) (*Parser, error) { + var def *RegexesDefinitions + + if err := yaml.Unmarshal(data, def); err != nil { + return nil, err + } + + return New(WithRegexesDefinitions(def)) +} From d635dc8ff752ee467fa847dc713ad59a344e288a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 09:30:12 +0200 Subject: [PATCH 07/20] Update uaparser/deprecated.go Co-authored-by: David Goldstein --- uaparser/deprecated.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaparser/deprecated.go b/uaparser/deprecated.go index 56c8ad0..e9acdc9 100644 --- a/uaparser/deprecated.go +++ b/uaparser/deprecated.go @@ -45,7 +45,7 @@ func NewFromSaved() *Parser { } // NewFromBytes is deprecated. -// Deprecated: Use New and option functions instead. +// Deprecated: Use New(WithRegexDefintions(...)) instead func NewFromBytes(data []byte) (*Parser, error) { var def *RegexesDefinitions From 0447e9d80a45cb46839e9a2ae8686e9f37941de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 09:30:34 +0200 Subject: [PATCH 08/20] Update uaparser/deprecated.go Co-authored-by: David Goldstein --- uaparser/deprecated.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaparser/deprecated.go b/uaparser/deprecated.go index e9acdc9..0b6dad2 100644 --- a/uaparser/deprecated.go +++ b/uaparser/deprecated.go @@ -32,7 +32,7 @@ func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debug } // NewFromSaved is deprecated. -// Deprecated: Use New and option functions instead. +// Deprecated: Use New() instead. func NewFromSaved() *Parser { parser, err := New() if err != nil { From db9373c557a0d8d900dd41a05c275c403a361ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 09:41:50 +0200 Subject: [PATCH 09/20] fix tests --- uaparser/benchmark_test.go | 4 ++-- uaparser/deprecated.go | 4 ++-- uaparser/parser.go | 2 +- uaparser/parsing_test.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/uaparser/benchmark_test.go b/uaparser/benchmark_test.go index e46b987..de146f2 100644 --- a/uaparser/benchmark_test.go +++ b/uaparser/benchmark_test.go @@ -23,7 +23,7 @@ func init() { log.Fatal(err) } - var def *RegexesDefinitions + def := &RegexesDefinitions{} if err := yaml.Unmarshal(regexes, def); err != nil { log.Fatal(err) @@ -77,7 +77,7 @@ func BenchmarkParserWithDifferentCacheSize(b *testing.B) { log.Fatal(err) } - var def *RegexesDefinitions + def := &RegexesDefinitions{} if err := yaml.Unmarshal(regexes, def); err != nil { log.Fatal(err) diff --git a/uaparser/deprecated.go b/uaparser/deprecated.go index 0b6dad2..2fdfe90 100644 --- a/uaparser/deprecated.go +++ b/uaparser/deprecated.go @@ -14,7 +14,7 @@ func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debug return nil, err } - var def *RegexesDefinitions + def := &RegexesDefinitions{} if err := yaml.Unmarshal(uaRegexes, def); err != nil { return nil, err @@ -47,7 +47,7 @@ func NewFromSaved() *Parser { // NewFromBytes is deprecated. // Deprecated: Use New(WithRegexDefintions(...)) instead func NewFromBytes(data []byte) (*Parser, error) { - var def *RegexesDefinitions + def := &RegexesDefinitions{} if err := yaml.Unmarshal(data, def); err != nil { return nil, err diff --git a/uaparser/parser.go b/uaparser/parser.go index cebd284..e62d68d 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -12,7 +12,7 @@ import ( ) var defaultRegexesDefinitions = sync.OnceValue(func() *RegexesDefinitions { - var def *RegexesDefinitions + def := &RegexesDefinitions{} if err := yaml.Unmarshal(DefinitionYaml, def); err != nil { panic(fmt.Errorf("error parsing regexes definitions: %w", err)) } diff --git a/uaparser/parsing_test.go b/uaparser/parsing_test.go index 1064a12..63db5c5 100644 --- a/uaparser/parsing_test.go +++ b/uaparser/parsing_test.go @@ -18,7 +18,7 @@ func init() { log.Fatal(err) } - var def *RegexesDefinitions + def := &RegexesDefinitions{} if err := yaml.Unmarshal(uaRegexes, def); err != nil { log.Fatal(err) From 5c9845f17b12e640debdca43ffea2b73e3c5617d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 09:47:38 +0200 Subject: [PATCH 10/20] WithRegexesDefinitions: Use a copied value of the struct --- uaparser/options.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uaparser/options.go b/uaparser/options.go index b595daf..c09d06b 100644 --- a/uaparser/options.go +++ b/uaparser/options.go @@ -38,8 +38,8 @@ func WithMatchIdxNotOk(idx int) Option { } } -func WithRegexesDefinitions(def *RegexesDefinitions) Option { +func WithRegexesDefinitions(def RegexesDefinitions) Option { return func(s *Parser) { - s.RegexesDefinitions = def + s.RegexesDefinitions = &def } } From 6bb80704e6923042c94de5a45d53a15bd4492da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 09:50:30 +0200 Subject: [PATCH 11/20] WithRegexesDefinitions: Use a copied value of the struct --- uaparser/benchmark_test.go | 8 ++++---- uaparser/deprecated.go | 8 ++++---- uaparser/parser.go | 18 +++++++++--------- uaparser/parsing_test.go | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/uaparser/benchmark_test.go b/uaparser/benchmark_test.go index de146f2..5bad391 100644 --- a/uaparser/benchmark_test.go +++ b/uaparser/benchmark_test.go @@ -23,9 +23,9 @@ func init() { log.Fatal(err) } - def := &RegexesDefinitions{} + var def RegexesDefinitions - if err := yaml.Unmarshal(regexes, def); err != nil { + if err := yaml.Unmarshal(regexes, &def); err != nil { log.Fatal(err) } @@ -77,9 +77,9 @@ func BenchmarkParserWithDifferentCacheSize(b *testing.B) { log.Fatal(err) } - def := &RegexesDefinitions{} + var def RegexesDefinitions - if err := yaml.Unmarshal(regexes, def); err != nil { + if err := yaml.Unmarshal(regexes, &def); err != nil { log.Fatal(err) } diff --git a/uaparser/deprecated.go b/uaparser/deprecated.go index 2fdfe90..b11783f 100644 --- a/uaparser/deprecated.go +++ b/uaparser/deprecated.go @@ -14,9 +14,9 @@ func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debug return nil, err } - def := &RegexesDefinitions{} + var def RegexesDefinitions - if err := yaml.Unmarshal(uaRegexes, def); err != nil { + if err := yaml.Unmarshal(uaRegexes, &def); err != nil { return nil, err } @@ -47,9 +47,9 @@ func NewFromSaved() *Parser { // NewFromBytes is deprecated. // Deprecated: Use New(WithRegexDefintions(...)) instead func NewFromBytes(data []byte) (*Parser, error) { - def := &RegexesDefinitions{} + var def RegexesDefinitions - if err := yaml.Unmarshal(data, def); err != nil { + if err := yaml.Unmarshal(data, &def); err != nil { return nil, err } diff --git a/uaparser/parser.go b/uaparser/parser.go index e62d68d..5653fd7 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -216,15 +216,15 @@ func New(options ...Option) (*Parser, error) { } func (parser *Parser) mustCompile() { // until we can use yaml.UnmarshalYAML with embedded pointer struct - for _, p := range parser.UA { + for _, p := range parser.RegexesDefinitions.UA { p.Reg = compileRegex(p.Flags, p.Expr) p.setDefaults() } - for _, p := range parser.OS { + for _, p := range parser.RegexesDefinitions.OS { p.Reg = compileRegex(p.Flags, p.Expr) p.setDefaults() } - for _, p := range parser.Device { + for _, p := range parser.RegexesDefinitions.Device { p.Reg = compileRegex(p.Flags, p.Expr) p.setDefaults() } @@ -275,7 +275,7 @@ func (parser *Parser) ParseUserAgent(line string) *UserAgent { ua := new(UserAgent) foundIdx := -1 found := false - for i, uaPattern := range parser.UA { + for i, uaPattern := range parser.RegexesDefinitions.UA { uaPattern.Match(line, ua) if len(ua.Family) > 0 { found = true @@ -303,7 +303,7 @@ func (parser *Parser) ParseOs(line string) *Os { os := new(Os) foundIdx := -1 found := false - for i, osPattern := range parser.OS { + for i, osPattern := range parser.RegexesDefinitions.OS { osPattern.Match(line, os) if len(os.Family) > 0 { found = true @@ -332,7 +332,7 @@ func (parser *Parser) ParseDevice(line string) *Device { dvc := new(Device) foundIdx := -1 found := false - for i, dvcPattern := range parser.Device { + for i, dvcPattern := range parser.RegexesDefinitions.Device { dvcPattern.Match(line, dvc) if len(dvc.Family) > 0 { found = true @@ -359,7 +359,7 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting UserAgents slice\n", time.Now()) } parser.UserAgentMisses = 0 - sort.Sort(UserAgentSorter(parser.UA)) + sort.Sort(UserAgentSorter(parser.RegexesDefinitions.UA)) } parser.mu.Unlock() parser.mu.Lock() @@ -368,7 +368,7 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting OS slice\n", time.Now()) } parser.OsMisses = 0 - sort.Sort(OsSorter(parser.OS)) + sort.Sort(OsSorter(parser.RegexesDefinitions.OS)) } parser.mu.Unlock() parser.mu.Lock() @@ -377,7 +377,7 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting Device slice\n", time.Now()) } parser.DeviceMisses = 0 - sort.Sort(DeviceSorter(parser.Device)) + sort.Sort(DeviceSorter(parser.RegexesDefinitions.Device)) } parser.mu.Unlock() } diff --git a/uaparser/parsing_test.go b/uaparser/parsing_test.go index 63db5c5..868d0d1 100644 --- a/uaparser/parsing_test.go +++ b/uaparser/parsing_test.go @@ -18,9 +18,9 @@ func init() { log.Fatal(err) } - def := &RegexesDefinitions{} + var def RegexesDefinitions - if err := yaml.Unmarshal(uaRegexes, def); err != nil { + if err := yaml.Unmarshal(uaRegexes, &def); err != nil { log.Fatal(err) } From c6642fc9ce576bd73399b535f6539b099495ae7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 09:53:37 +0200 Subject: [PATCH 12/20] defaultRegexesDefinitions always return a value to keep the immutability --- uaparser/parser.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/uaparser/parser.go b/uaparser/parser.go index 5653fd7..36acb71 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -11,9 +11,9 @@ import ( "gopkg.in/yaml.v3" ) -var defaultRegexesDefinitions = sync.OnceValue(func() *RegexesDefinitions { - def := &RegexesDefinitions{} - if err := yaml.Unmarshal(DefinitionYaml, def); err != nil { +var defaultRegexesDefinitions = sync.OnceValue(func() RegexesDefinitions { + var def RegexesDefinitions + if err := yaml.Unmarshal(DefinitionYaml, &def); err != nil { panic(fmt.Errorf("error parsing regexes definitions: %w", err)) } @@ -207,7 +207,8 @@ func New(options ...Option) (*Parser, error) { } if parser.RegexesDefinitions == nil { - parser.RegexesDefinitions = defaultRegexesDefinitions() + regexesDefinitions := defaultRegexesDefinitions() + parser.RegexesDefinitions = ®exesDefinitions } parser.mustCompile() From eccf038b46c822fadc1f9266e836b098281e855f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sun, 19 Oct 2025 09:58:27 +0200 Subject: [PATCH 13/20] RegexesDefinitions -> RegexDefinitions --- test.go | 12 ++++++------ uaparser/benchmark_test.go | 10 +++++----- uaparser/deprecated.go | 10 +++++----- uaparser/options.go | 4 ++-- uaparser/parser.go | 30 +++++++++++++++--------------- uaparser/parsing_test.go | 4 ++-- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/test.go b/test.go index 7beb8c4..511e0c4 100644 --- a/test.go +++ b/test.go @@ -24,9 +24,9 @@ func main() { return } - var def *uaparser.RegexesDefinitions + var def uaparser.RegexDefinitions - if err := yaml.Unmarshal(regexes, def); err != nil { + if err := yaml.Unmarshal(regexes, &def); err != nil { fmt.Printf("error parsing regexes definitions. Error: %s\n", err.Error()) return } @@ -37,7 +37,7 @@ func main() { case "new": fmt.Println("Running new version of uap...") uaParser, _ := uaparser.New( - uaparser.WithRegexesDefinitions(def), + uaparser.WithRegexDefinitions(def), uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), uaparser.WithMissesThreshold(100), uaparser.WithMatchIdxNotOk(20), @@ -53,7 +53,7 @@ func main() { return case "old": fmt.Println("Running old version of uap...") - uaParser, _ := uaparser.New(uaparser.WithRegexesDefinitions(def)) + uaParser, _ := uaparser.New(uaparser.WithRegexDefinitions(def)) for i := 0; i < cLevel; i++ { wg.Add(1) go runTest(uaParser, i, &wg) @@ -63,7 +63,7 @@ func main() { case "both": fmt.Println("Running new version of uap...") uaParser, _ := uaparser.New( - uaparser.WithRegexesDefinitions(def), + uaparser.WithRegexDefinitions(def), uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), uaparser.WithMissesThreshold(100), uaparser.WithMatchIdxNotOk(20), @@ -76,7 +76,7 @@ func main() { runTest(uaParser, i, &wg) } fmt.Println("Running old version of uap...") - uaParser, _ = uaparser.New(uaparser.WithRegexesDefinitions(def)) + uaParser, _ = uaparser.New(uaparser.WithRegexDefinitions(def)) for i := 0; i < cLevel; i++ { wg.Add(1) runTest(uaParser, i, &wg) diff --git a/uaparser/benchmark_test.go b/uaparser/benchmark_test.go index 5bad391..eaa7c4a 100644 --- a/uaparser/benchmark_test.go +++ b/uaparser/benchmark_test.go @@ -23,17 +23,17 @@ func init() { log.Fatal(err) } - var def RegexesDefinitions + var def RegexDefinitions if err := yaml.Unmarshal(regexes, &def); err != nil { log.Fatal(err) } - benchedParser, err = New(WithRegexesDefinitions(def)) + benchedParser, err = New(WithRegexDefinitions(def)) if err != nil { log.Fatal(err) } - benchedParserWithOptions, err = New(WithRegexesDefinitions(def), + benchedParserWithOptions, err = New(WithRegexDefinitions(def), WithMode(EOsLookUpMode|EUserAgentLookUpMode), WithMissesThreshold(100), WithMatchIdxNotOk(20), @@ -77,14 +77,14 @@ func BenchmarkParserWithDifferentCacheSize(b *testing.B) { log.Fatal(err) } - var def RegexesDefinitions + var def RegexDefinitions if err := yaml.Unmarshal(regexes, &def); err != nil { log.Fatal(err) } for _, size := range sizes { - parser, err := New(WithRegexesDefinitions(def), + parser, err := New(WithRegexDefinitions(def), WithMode(EOsLookUpMode|EUserAgentLookUpMode|EDeviceLookUpMode), WithMissesThreshold(cDefaultMissesTreshold), WithMatchIdxNotOk(cDefaultMatchIdxNotOk), diff --git a/uaparser/deprecated.go b/uaparser/deprecated.go index b11783f..0be62f5 100644 --- a/uaparser/deprecated.go +++ b/uaparser/deprecated.go @@ -14,14 +14,14 @@ func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debug return nil, err } - var def RegexesDefinitions + var def RegexDefinitions if err := yaml.Unmarshal(uaRegexes, &def); err != nil { return nil, err } return New( - WithRegexesDefinitions(def), + WithRegexDefinitions(def), WithMode(LookupMode(mode)), WithMissesThreshold(uint64(treshold)), WithMatchIdxNotOk(topCnt), @@ -45,13 +45,13 @@ func NewFromSaved() *Parser { } // NewFromBytes is deprecated. -// Deprecated: Use New(WithRegexDefintions(...)) instead +// Deprecated: Use New(WithRegexDefinitions(...)) instead func NewFromBytes(data []byte) (*Parser, error) { - var def RegexesDefinitions + var def RegexDefinitions if err := yaml.Unmarshal(data, &def); err != nil { return nil, err } - return New(WithRegexesDefinitions(def)) + return New(WithRegexDefinitions(def)) } diff --git a/uaparser/options.go b/uaparser/options.go index c09d06b..5e675ef 100644 --- a/uaparser/options.go +++ b/uaparser/options.go @@ -38,8 +38,8 @@ func WithMatchIdxNotOk(idx int) Option { } } -func WithRegexesDefinitions(def RegexesDefinitions) Option { +func WithRegexDefinitions(def RegexDefinitions) Option { return func(s *Parser) { - s.RegexesDefinitions = &def + s.RegexDefinitions = &def } } diff --git a/uaparser/parser.go b/uaparser/parser.go index 36acb71..d0da86d 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -11,8 +11,8 @@ import ( "gopkg.in/yaml.v3" ) -var defaultRegexesDefinitions = sync.OnceValue(func() RegexesDefinitions { - var def RegexesDefinitions +var defaultRegexesDefinitions = sync.OnceValue(func() RegexDefinitions { + var def RegexDefinitions if err := yaml.Unmarshal(DefinitionYaml, &def); err != nil { panic(fmt.Errorf("error parsing regexes definitions: %w", err)) } @@ -20,7 +20,7 @@ var defaultRegexesDefinitions = sync.OnceValue(func() RegexesDefinitions { return def }) -type RegexesDefinitions struct { +type RegexDefinitions struct { UA []*uaParser `yaml:"user_agent_parsers"` OS []*osParser `yaml:"os_parsers"` Device []*deviceParser `yaml:"device_parsers"` @@ -154,7 +154,7 @@ type Parser struct { config *parserConfig cache *cache - *RegexesDefinitions + *RegexDefinitions mu *sync.RWMutex } @@ -206,9 +206,9 @@ func New(options ...Option) (*Parser, error) { parser.cache = newCache(parser.config.CacheSize) } - if parser.RegexesDefinitions == nil { + if parser.RegexDefinitions == nil { regexesDefinitions := defaultRegexesDefinitions() - parser.RegexesDefinitions = ®exesDefinitions + parser.RegexDefinitions = ®exesDefinitions } parser.mustCompile() @@ -217,15 +217,15 @@ func New(options ...Option) (*Parser, error) { } func (parser *Parser) mustCompile() { // until we can use yaml.UnmarshalYAML with embedded pointer struct - for _, p := range parser.RegexesDefinitions.UA { + for _, p := range parser.RegexDefinitions.UA { p.Reg = compileRegex(p.Flags, p.Expr) p.setDefaults() } - for _, p := range parser.RegexesDefinitions.OS { + for _, p := range parser.RegexDefinitions.OS { p.Reg = compileRegex(p.Flags, p.Expr) p.setDefaults() } - for _, p := range parser.RegexesDefinitions.Device { + for _, p := range parser.RegexDefinitions.Device { p.Reg = compileRegex(p.Flags, p.Expr) p.setDefaults() } @@ -276,7 +276,7 @@ func (parser *Parser) ParseUserAgent(line string) *UserAgent { ua := new(UserAgent) foundIdx := -1 found := false - for i, uaPattern := range parser.RegexesDefinitions.UA { + for i, uaPattern := range parser.RegexDefinitions.UA { uaPattern.Match(line, ua) if len(ua.Family) > 0 { found = true @@ -304,7 +304,7 @@ func (parser *Parser) ParseOs(line string) *Os { os := new(Os) foundIdx := -1 found := false - for i, osPattern := range parser.RegexesDefinitions.OS { + for i, osPattern := range parser.RegexDefinitions.OS { osPattern.Match(line, os) if len(os.Family) > 0 { found = true @@ -333,7 +333,7 @@ func (parser *Parser) ParseDevice(line string) *Device { dvc := new(Device) foundIdx := -1 found := false - for i, dvcPattern := range parser.RegexesDefinitions.Device { + for i, dvcPattern := range parser.RegexDefinitions.Device { dvcPattern.Match(line, dvc) if len(dvc.Family) > 0 { found = true @@ -360,7 +360,7 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting UserAgents slice\n", time.Now()) } parser.UserAgentMisses = 0 - sort.Sort(UserAgentSorter(parser.RegexesDefinitions.UA)) + sort.Sort(UserAgentSorter(parser.RegexDefinitions.UA)) } parser.mu.Unlock() parser.mu.Lock() @@ -369,7 +369,7 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting OS slice\n", time.Now()) } parser.OsMisses = 0 - sort.Sort(OsSorter(parser.RegexesDefinitions.OS)) + sort.Sort(OsSorter(parser.RegexDefinitions.OS)) } parser.mu.Unlock() parser.mu.Lock() @@ -378,7 +378,7 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting Device slice\n", time.Now()) } parser.DeviceMisses = 0 - sort.Sort(DeviceSorter(parser.RegexesDefinitions.Device)) + sort.Sort(DeviceSorter(parser.RegexDefinitions.Device)) } parser.mu.Unlock() } diff --git a/uaparser/parsing_test.go b/uaparser/parsing_test.go index 868d0d1..975c1a1 100644 --- a/uaparser/parsing_test.go +++ b/uaparser/parsing_test.go @@ -18,13 +18,13 @@ func init() { log.Fatal(err) } - var def RegexesDefinitions + var def RegexDefinitions if err := yaml.Unmarshal(uaRegexes, &def); err != nil { log.Fatal(err) } - testParser, err = New(WithRegexesDefinitions(def)) + testParser, err = New(WithRegexDefinitions(def)) if err != nil { log.Fatal(err) } From 7c220ba804469e9b9fa215d575d9973e6b62c741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Mon, 20 Oct 2025 08:50:55 +0200 Subject: [PATCH 14/20] Update uaparser/parser.go Co-authored-by: David Goldstein --- uaparser/parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uaparser/parser.go b/uaparser/parser.go index d0da86d..94a94eb 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -180,7 +180,7 @@ func New(options ...Option) (*Parser, error) { UseSort: cDefaultSortOption, DebugMode: cDefaultDebugMode, CacheSize: cDefaultCacheSize, - MissesThreshold: cMinMissesTreshold, + MissesThreshold: cDefaultMissesTreshold, MatchIdxNotOk: cDefaultMatchIdxNotOk, }, mu: &sync.RWMutex{}, From 4d5273c7fe054feeabef1d4430c601dd9692555b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Mon, 20 Oct 2025 08:54:55 +0200 Subject: [PATCH 15/20] WithMissesThreshold is a SortOption now --- test.go | 6 ++---- uaparser/benchmark_test.go | 6 ++---- uaparser/deprecated.go | 3 +-- uaparser/options.go | 20 +++++++++++++------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/test.go b/test.go index 511e0c4..4183e36 100644 --- a/test.go +++ b/test.go @@ -39,9 +39,8 @@ func main() { uaParser, _ := uaparser.New( uaparser.WithRegexDefinitions(def), uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), - uaparser.WithMissesThreshold(100), uaparser.WithMatchIdxNotOk(20), - uaparser.WithSort(true), + uaparser.WithSort(true, uaparser.WithMissesThreshold(100)), uaparser.WithDebug(true), uaparser.WithCacheSize(1024), ) @@ -65,9 +64,8 @@ func main() { uaParser, _ := uaparser.New( uaparser.WithRegexDefinitions(def), uaparser.WithMode(uaparser.EOsLookUpMode|uaparser.EUserAgentLookUpMode), - uaparser.WithMissesThreshold(100), uaparser.WithMatchIdxNotOk(20), - uaparser.WithSort(true), + uaparser.WithSort(true, uaparser.WithMissesThreshold(100)), uaparser.WithDebug(true), uaparser.WithCacheSize(1024), ) diff --git a/uaparser/benchmark_test.go b/uaparser/benchmark_test.go index eaa7c4a..953959d 100644 --- a/uaparser/benchmark_test.go +++ b/uaparser/benchmark_test.go @@ -35,9 +35,8 @@ func init() { } benchedParserWithOptions, err = New(WithRegexDefinitions(def), WithMode(EOsLookUpMode|EUserAgentLookUpMode), - WithMissesThreshold(100), WithMatchIdxNotOk(20), - WithSort(true), + WithSort(true, WithMissesThreshold(100)), WithDebug(true), WithCacheSize(cDefaultCacheSize), ) @@ -86,9 +85,8 @@ func BenchmarkParserWithDifferentCacheSize(b *testing.B) { for _, size := range sizes { parser, err := New(WithRegexDefinitions(def), WithMode(EOsLookUpMode|EUserAgentLookUpMode|EDeviceLookUpMode), - WithMissesThreshold(cDefaultMissesTreshold), WithMatchIdxNotOk(cDefaultMatchIdxNotOk), - WithSort(false), + WithSort(false, WithMissesThreshold(cDefaultMissesTreshold)), WithDebug(false), WithCacheSize(size), ) diff --git a/uaparser/deprecated.go b/uaparser/deprecated.go index 0be62f5..886a9e2 100644 --- a/uaparser/deprecated.go +++ b/uaparser/deprecated.go @@ -23,9 +23,8 @@ func NewWithOptions(regexFile string, mode, treshold, topCnt int, useSort, debug return New( WithRegexDefinitions(def), WithMode(LookupMode(mode)), - WithMissesThreshold(uint64(treshold)), WithMatchIdxNotOk(topCnt), - WithSort(useSort), + WithSort(useSort, WithMissesThreshold(uint64(treshold))), WithDebug(debugMode), WithCacheSize(cacheSize), ) diff --git a/uaparser/options.go b/uaparser/options.go index 5e675ef..696d4df 100644 --- a/uaparser/options.go +++ b/uaparser/options.go @@ -8,9 +8,13 @@ func WithMode(mode LookupMode) Option { } } -func WithSort(useSort bool) Option { +func WithSort(useSort bool, options ...SortOption) Option { return func(s *Parser) { s.config.UseSort = useSort + + for _, o := range options { + o(s) + } } } @@ -26,12 +30,6 @@ func WithCacheSize(size int) Option { } } -func WithMissesThreshold(threshold uint64) Option { - return func(s *Parser) { - s.config.MissesThreshold = threshold - } -} - func WithMatchIdxNotOk(idx int) Option { return func(s *Parser) { s.config.MatchIdxNotOk = idx @@ -43,3 +41,11 @@ func WithRegexDefinitions(def RegexDefinitions) Option { s.RegexDefinitions = &def } } + +type SortOption func(*Parser) + +func WithMissesThreshold(threshold uint64) SortOption { + return func(s *Parser) { + s.config.MissesThreshold = threshold + } +} From 6b9accc2c0705d23f87096210211e5cc1b249d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Mon, 20 Oct 2025 09:02:47 +0200 Subject: [PATCH 16/20] create a copy before sort --- uaparser/parser.go | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/uaparser/parser.go b/uaparser/parser.go index 94a94eb..0ef28c0 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -360,7 +360,14 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting UserAgents slice\n", time.Now()) } parser.UserAgentMisses = 0 - sort.Sort(UserAgentSorter(parser.RegexDefinitions.UA)) + + // create a copy to avoid modifying the original slice while in use. + uaDefinitions := make([]*uaParser, len(parser.RegexDefinitions.UA)) + copy(uaDefinitions, parser.RegexDefinitions.UA) + + sort.Sort(UserAgentSorter(uaDefinitions)) + + parser.RegexDefinitions.UA = uaDefinitions } parser.mu.Unlock() parser.mu.Lock() @@ -369,7 +376,14 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting OS slice\n", time.Now()) } parser.OsMisses = 0 - sort.Sort(OsSorter(parser.RegexDefinitions.OS)) + + // create a copy to avoid modifying the original slice while in use. + osDefinitions := make([]*osParser, len(parser.RegexDefinitions.OS)) + copy(osDefinitions, parser.RegexDefinitions.OS) + + sort.Sort(OsSorter(osDefinitions)) + + parser.RegexDefinitions.OS = osDefinitions } parser.mu.Unlock() parser.mu.Lock() @@ -378,7 +392,14 @@ func checkAndSort(parser *Parser) { fmt.Printf("%s\tSorting Device slice\n", time.Now()) } parser.DeviceMisses = 0 - sort.Sort(DeviceSorter(parser.RegexDefinitions.Device)) + + // create a copy to avoid modifying the original slice while in use. + deviceDefinitions := make([]*deviceParser, len(parser.RegexDefinitions.Device)) + copy(deviceDefinitions, parser.RegexDefinitions.Device) + + sort.Sort(DeviceSorter(deviceDefinitions)) + + parser.RegexDefinitions.Device = deviceDefinitions } parser.mu.Unlock() } From bfbb1564387689de1cf8e54e74b0fdc802bfe980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 5 Dec 2025 09:11:20 +0100 Subject: [PATCH 17/20] deep copy RegexDefinitions, if use sorted is enabled --- uaparser/device.go | 10 ++-- uaparser/os.go | 14 ++--- uaparser/parser.go | 113 ++++++++++++++++++++++++++++++++--------- uaparser/user_agent.go | 12 ++--- 4 files changed, 106 insertions(+), 43 deletions(-) diff --git a/uaparser/device.go b/uaparser/device.go index 7a115e7..13d9ed4 100644 --- a/uaparser/device.go +++ b/uaparser/device.go @@ -8,20 +8,20 @@ type Device struct { Model string } -func (parser *deviceParser) Match(line string, dvc *Device) { - matches := parser.Reg.FindStringSubmatchIndex(line) +func (dp *deviceParser) Match(line string, dvc *Device) { + matches := dp.Reg.FindStringSubmatchIndex(line) if len(matches) == 0 { return } - dvc.Family = string(parser.Reg.ExpandString(nil, parser.DeviceReplacement, line, matches)) + dvc.Family = string(dp.Reg.ExpandString(nil, dp.DeviceReplacement, line, matches)) dvc.Family = strings.TrimSpace(dvc.Family) - dvc.Brand = string(parser.Reg.ExpandString(nil, parser.BrandReplacement, line, matches)) + dvc.Brand = string(dp.Reg.ExpandString(nil, dp.BrandReplacement, line, matches)) dvc.Brand = strings.TrimSpace(dvc.Brand) - dvc.Model = string(parser.Reg.ExpandString(nil, parser.ModelReplacement, line, matches)) + dvc.Model = string(dp.Reg.ExpandString(nil, dp.ModelReplacement, line, matches)) dvc.Model = strings.TrimSpace(dvc.Model) } diff --git a/uaparser/os.go b/uaparser/os.go index b30e8a1..2d33e94 100644 --- a/uaparser/os.go +++ b/uaparser/os.go @@ -8,14 +8,14 @@ type Os struct { PatchMinor string `yaml:"patch_minor"` } -func (parser *osParser) Match(line string, os *Os) { - matches := parser.Reg.FindStringSubmatchIndex(line) +func (osp *osParser) Match(line string, os *Os) { + matches := osp.Reg.FindStringSubmatchIndex(line) if len(matches) > 0 { - os.Family = string(parser.Reg.ExpandString(nil, parser.OSReplacement, line, matches)) - os.Major = string(parser.Reg.ExpandString(nil, parser.V1Replacement, line, matches)) - os.Minor = string(parser.Reg.ExpandString(nil, parser.V2Replacement, line, matches)) - os.Patch = string(parser.Reg.ExpandString(nil, parser.V3Replacement, line, matches)) - os.PatchMinor = string(parser.Reg.ExpandString(nil, parser.V4Replacement, line, matches)) + os.Family = string(osp.Reg.ExpandString(nil, osp.OSReplacement, line, matches)) + os.Major = string(osp.Reg.ExpandString(nil, osp.V1Replacement, line, matches)) + os.Minor = string(osp.Reg.ExpandString(nil, osp.V2Replacement, line, matches)) + os.Patch = string(osp.Reg.ExpandString(nil, osp.V3Replacement, line, matches)) + os.PatchMinor = string(osp.Reg.ExpandString(nil, osp.V4Replacement, line, matches)) } } diff --git a/uaparser/parser.go b/uaparser/parser.go index 0ef28c0..ddf422e 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -26,6 +26,32 @@ type RegexDefinitions struct { Device []*deviceParser `yaml:"device_parsers"` } +func (rd *RegexDefinitions) Clone() *RegexDefinitions { + if rd == nil { + return nil + } + + rd2 := &RegexDefinitions{ + UA: make([]*uaParser, len(rd.UA)), + OS: make([]*osParser, len(rd.OS)), + Device: make([]*deviceParser, len(rd.Device)), + } + + for i, v := range rd.UA { + rd2.UA[i] = v.Clone() + } + + for i, v := range rd.OS { + rd2.OS[i] = v.Clone() + } + + for i, v := range rd.Device { + rd2.Device[i] = v.Clone() + } + + return rd2 +} + type UserAgentSorter []*uaParser func (a UserAgentSorter) Len() int { return len(a) } @@ -46,18 +72,29 @@ type uaParser struct { MatchesCount uint64 } -func (ua *uaParser) setDefaults() { - if ua.FamilyReplacement == "" { - ua.FamilyReplacement = "$1" +func (uap *uaParser) Clone() *uaParser { + if uap == nil { + return nil + } + + ua2 := *uap + ua2.MatchesCount = 0 + + return &ua2 +} + +func (uap *uaParser) setDefaults() { + if uap.FamilyReplacement == "" { + uap.FamilyReplacement = "$1" } - if ua.V1Replacement == "" { - ua.V1Replacement = "$2" + if uap.V1Replacement == "" { + uap.V1Replacement = "$2" } - if ua.V2Replacement == "" { - ua.V2Replacement = "$3" + if uap.V2Replacement == "" { + uap.V2Replacement = "$3" } - if ua.V3Replacement == "" { - ua.V3Replacement = "$4" + if uap.V3Replacement == "" { + uap.V3Replacement = "$4" } } @@ -82,21 +119,32 @@ type osParser struct { MatchesCount uint64 } -func (os *osParser) setDefaults() { - if os.OSReplacement == "" { - os.OSReplacement = "$1" +func (osp *osParser) Clone() *osParser { + if osp == nil { + return nil + } + + os2 := *osp + os2.MatchesCount = 0 + + return &os2 +} + +func (osp *osParser) setDefaults() { + if osp.OSReplacement == "" { + osp.OSReplacement = "$1" } - if os.V1Replacement == "" { - os.V1Replacement = "$2" + if osp.V1Replacement == "" { + osp.V1Replacement = "$2" } - if os.V2Replacement == "" { - os.V2Replacement = "$3" + if osp.V2Replacement == "" { + osp.V2Replacement = "$3" } - if os.V3Replacement == "" { - os.V3Replacement = "$4" + if osp.V3Replacement == "" { + osp.V3Replacement = "$4" } - if os.V4Replacement == "" { - os.V4Replacement = "$5" + if osp.V4Replacement == "" { + osp.V4Replacement = "$5" } } @@ -119,12 +167,23 @@ type deviceParser struct { MatchesCount uint64 } -func (device *deviceParser) setDefaults() { - if device.DeviceReplacement == "" { - device.DeviceReplacement = "$1" +func (dp *deviceParser) Clone() *deviceParser { + if dp == nil { + return nil } - if device.ModelReplacement == "" { - device.ModelReplacement = "$1" + + device2 := *dp + device2.MatchesCount = 0 + + return &device2 +} + +func (dp *deviceParser) setDefaults() { + if dp.DeviceReplacement == "" { + dp.DeviceReplacement = "$1" + } + if dp.ModelReplacement == "" { + dp.ModelReplacement = "$1" } } @@ -211,6 +270,10 @@ func New(options ...Option) (*Parser, error) { parser.RegexDefinitions = ®exesDefinitions } + if parser.config.UseSort { + parser.RegexDefinitions = parser.RegexDefinitions.Clone() + } + parser.mustCompile() return parser, nil diff --git a/uaparser/user_agent.go b/uaparser/user_agent.go index ad4073a..569b56a 100644 --- a/uaparser/user_agent.go +++ b/uaparser/user_agent.go @@ -7,13 +7,13 @@ type UserAgent struct { Patch string } -func (parser *uaParser) Match(line string, ua *UserAgent) { - matches := parser.Reg.FindStringSubmatchIndex(line) +func (uap *uaParser) Match(line string, ua *UserAgent) { + matches := uap.Reg.FindStringSubmatchIndex(line) if len(matches) > 0 { - ua.Family = string(parser.Reg.ExpandString(nil, parser.FamilyReplacement, line, matches)) - ua.Major = string(parser.Reg.ExpandString(nil, parser.V1Replacement, line, matches)) - ua.Minor = string(parser.Reg.ExpandString(nil, parser.V2Replacement, line, matches)) - ua.Patch = string(parser.Reg.ExpandString(nil, parser.V3Replacement, line, matches)) + ua.Family = string(uap.Reg.ExpandString(nil, uap.FamilyReplacement, line, matches)) + ua.Major = string(uap.Reg.ExpandString(nil, uap.V1Replacement, line, matches)) + ua.Minor = string(uap.Reg.ExpandString(nil, uap.V2Replacement, line, matches)) + ua.Patch = string(uap.Reg.ExpandString(nil, uap.V3Replacement, line, matches)) } } From 21fc7bac766af7d459c15061e4dcb66e671e5a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sat, 6 Dec 2025 09:42:43 +0100 Subject: [PATCH 18/20] Update uaparser/parser.go Co-authored-by: David Goldstein --- uaparser/parser.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/uaparser/parser.go b/uaparser/parser.go index ddf422e..cb562d2 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -424,13 +424,7 @@ func checkAndSort(parser *Parser) { } parser.UserAgentMisses = 0 - // create a copy to avoid modifying the original slice while in use. - uaDefinitions := make([]*uaParser, len(parser.RegexDefinitions.UA)) - copy(uaDefinitions, parser.RegexDefinitions.UA) - - sort.Sort(UserAgentSorter(uaDefinitions)) - - parser.RegexDefinitions.UA = uaDefinitions + sort.Sort(UserAgentSorter(parser.RegexDefinitions.UA)) } parser.mu.Unlock() parser.mu.Lock() From 6a257003cc621e0e349c197b1501043c5d321bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sat, 6 Dec 2025 09:43:18 +0100 Subject: [PATCH 19/20] Update uaparser/parser.go Co-authored-by: David Goldstein --- uaparser/parser.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/uaparser/parser.go b/uaparser/parser.go index cb562d2..f16e453 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -434,13 +434,7 @@ func checkAndSort(parser *Parser) { } parser.OsMisses = 0 - // create a copy to avoid modifying the original slice while in use. - osDefinitions := make([]*osParser, len(parser.RegexDefinitions.OS)) - copy(osDefinitions, parser.RegexDefinitions.OS) - - sort.Sort(OsSorter(osDefinitions)) - - parser.RegexDefinitions.OS = osDefinitions + sort.Sort(OsSorter(parser.RegexDefinitions.OS)) } parser.mu.Unlock() parser.mu.Lock() From 76a10f6cf6a19f664212ec46301bd541b062615a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Sat, 6 Dec 2025 09:43:30 +0100 Subject: [PATCH 20/20] Update uaparser/parser.go Co-authored-by: David Goldstein --- uaparser/parser.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/uaparser/parser.go b/uaparser/parser.go index f16e453..7c28b82 100644 --- a/uaparser/parser.go +++ b/uaparser/parser.go @@ -444,13 +444,7 @@ func checkAndSort(parser *Parser) { } parser.DeviceMisses = 0 - // create a copy to avoid modifying the original slice while in use. - deviceDefinitions := make([]*deviceParser, len(parser.RegexDefinitions.Device)) - copy(deviceDefinitions, parser.RegexDefinitions.Device) - - sort.Sort(DeviceSorter(deviceDefinitions)) - - parser.RegexDefinitions.Device = deviceDefinitions + sort.Sort(DeviceSorter(parser.RegexDefinitions.Device)) } parser.mu.Unlock() }