useragent.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. package parser
  2. import (
  3. "regexp"
  4. "strings"
  5. "sync"
  6. )
  7. // SimpleUserAgentParser implements a lightweight user agent parser
  8. type SimpleUserAgentParser struct {
  9. browserPatterns []browserPattern
  10. osPatterns []osPattern
  11. devicePatterns []devicePattern
  12. }
  13. type browserPattern struct {
  14. name string
  15. pattern *regexp.Regexp
  16. version *regexp.Regexp
  17. }
  18. type osPattern struct {
  19. name string
  20. pattern *regexp.Regexp
  21. version *regexp.Regexp
  22. }
  23. type devicePattern struct {
  24. name string
  25. pattern *regexp.Regexp
  26. }
  27. // NewSimpleUserAgentParser creates a new simple user agent parser
  28. func NewSimpleUserAgentParser() *SimpleUserAgentParser {
  29. return &SimpleUserAgentParser{
  30. browserPatterns: initBrowserPatterns(),
  31. osPatterns: initOSPatterns(),
  32. devicePatterns: initDevicePatterns(),
  33. }
  34. }
  35. func initBrowserPatterns() []browserPattern {
  36. return []browserPattern{
  37. // Bot detection (highest priority)
  38. {
  39. name: "Bot",
  40. pattern: regexp.MustCompile(`(?i)bot|crawler|spider|crawl|slurp|sohu-search|lycos|robozilla|googlebot|bingbot|facebookexternalhit|twitterbot|whatsapp|telegrambot|applebot|linkedinbot|pinterest|yandexbot|baiduspider|360spider|sogou|bytedance|tiktok`),
  41. version: nil,
  42. },
  43. // Mobile Apps and Special Browsers (high priority)
  44. {
  45. name: "WeChat",
  46. pattern: regexp.MustCompile(`(?i)micromessenger`),
  47. version: regexp.MustCompile(`(?i)micromessenger/(\d+\.\d+)`),
  48. },
  49. {
  50. name: "QQ",
  51. pattern: regexp.MustCompile(`(?i)qq/(\d+\.\d+)`),
  52. version: regexp.MustCompile(`(?i)qq/(\d+\.\d+)`),
  53. },
  54. {
  55. name: "DingTalk",
  56. pattern: regexp.MustCompile(`(?i)dingtalk`),
  57. version: regexp.MustCompile(`(?i)dingtalk/(\d+\.\d+)`),
  58. },
  59. {
  60. name: "Alipay",
  61. pattern: regexp.MustCompile(`(?i)alipayclient`),
  62. version: regexp.MustCompile(`(?i)alipayclient/(\d+\.\d+)`),
  63. },
  64. {
  65. name: "TikTok",
  66. pattern: regexp.MustCompile(`(?i)musically_`),
  67. version: regexp.MustCompile(`(?i)musically_(\d+\.\d+)`),
  68. },
  69. // Chinese Browsers
  70. {
  71. name: "360 Browser",
  72. pattern: regexp.MustCompile(`(?i)360se|qihoobrowser`),
  73. version: regexp.MustCompile(`(?i)360se/(\d+\.\d+)|qihoobrowser/(\d+\.\d+)`),
  74. },
  75. {
  76. name: "QQ Browser",
  77. pattern: regexp.MustCompile(`(?i)qqbrowser`),
  78. version: regexp.MustCompile(`(?i)qqbrowser/(\d+\.\d+)`),
  79. },
  80. {
  81. name: "UC Browser",
  82. pattern: regexp.MustCompile(`(?i)ucbrowser|uc browser`),
  83. version: regexp.MustCompile(`(?i)ucbrowser/(\d+\.\d+)`),
  84. },
  85. {
  86. name: "Sogou Explorer",
  87. pattern: regexp.MustCompile(`(?i)se |metasr`),
  88. version: regexp.MustCompile(`(?i)se (\d+\.\d+)|metasr (\d+\.\d+)`),
  89. },
  90. {
  91. name: "Baidu Browser",
  92. pattern: regexp.MustCompile(`(?i)baidubrowser|bidubrowser`),
  93. version: regexp.MustCompile(`(?i)baidubrowser/(\d+\.\d+)|bidubrowser/(\d+\.\d+)`),
  94. },
  95. {
  96. name: "Maxthon",
  97. pattern: regexp.MustCompile(`(?i)maxthon`),
  98. version: regexp.MustCompile(`(?i)maxthon/(\d+\.\d+)`),
  99. },
  100. // International Mobile Browsers
  101. {
  102. name: "Samsung Browser",
  103. pattern: regexp.MustCompile(`(?i)samsungbrowser`),
  104. version: regexp.MustCompile(`(?i)samsungbrowser/(\d+\.\d+)`),
  105. },
  106. {
  107. name: "Huawei Browser",
  108. pattern: regexp.MustCompile(`(?i)huaweibrowser`),
  109. version: regexp.MustCompile(`(?i)huaweibrowser/(\d+\.\d+)`),
  110. },
  111. {
  112. name: "Xiaomi Browser",
  113. pattern: regexp.MustCompile(`(?i)mibrowser`),
  114. version: regexp.MustCompile(`(?i)mibrowser/(\d+\.\d+)`),
  115. },
  116. {
  117. name: "Oppo Browser",
  118. pattern: regexp.MustCompile(`(?i)oppobrowser`),
  119. version: regexp.MustCompile(`(?i)oppobrowser/(\d+\.\d+)`),
  120. },
  121. {
  122. name: "Vivo Browser",
  123. pattern: regexp.MustCompile(`(?i)vivobrowser`),
  124. version: regexp.MustCompile(`(?i)vivobrowser/(\d+\.\d+)`),
  125. },
  126. // International Browsers
  127. {
  128. name: "Yandex",
  129. pattern: regexp.MustCompile(`(?i)yabrowser`),
  130. version: regexp.MustCompile(`(?i)yabrowser/(\d+\.\d+)`),
  131. },
  132. {
  133. name: "Brave",
  134. pattern: regexp.MustCompile(`(?i)brave`),
  135. version: regexp.MustCompile(`(?i)brave/(\d+\.\d+)`),
  136. },
  137. {
  138. name: "Vivaldi",
  139. pattern: regexp.MustCompile(`(?i)vivaldi`),
  140. version: regexp.MustCompile(`(?i)vivaldi/(\d+\.\d+)`),
  141. },
  142. // Microsoft Browsers (Order matters - Edge before IE)
  143. {
  144. name: "Edge",
  145. pattern: regexp.MustCompile(`(?i)edg/|edge/`),
  146. version: regexp.MustCompile(`(?i)edg?[e]?/(\d+\.\d+)`),
  147. },
  148. {
  149. name: "Internet Explorer",
  150. pattern: regexp.MustCompile(`(?i)msie |trident.*rv:`),
  151. version: regexp.MustCompile(`(?i)msie (\d+\.\d+)|rv:(\d+\.\d+)`),
  152. },
  153. // Major Browsers (Order matters - Chrome variants before Chrome)
  154. {
  155. name: "Opera",
  156. pattern: regexp.MustCompile(`(?i)opr/|opera/`),
  157. version: regexp.MustCompile(`(?i)opr/(\d+\.\d+)|version/(\d+\.\d+)`),
  158. },
  159. {
  160. name: "Chrome",
  161. pattern: regexp.MustCompile(`(?i)chrome/`),
  162. version: regexp.MustCompile(`(?i)chrome/(\d+\.\d+)`),
  163. },
  164. {
  165. name: "Firefox",
  166. pattern: regexp.MustCompile(`(?i)firefox/`),
  167. version: regexp.MustCompile(`(?i)firefox/(\d+\.\d+)`),
  168. },
  169. {
  170. name: "Safari",
  171. pattern: regexp.MustCompile(`(?i)safari/`),
  172. version: regexp.MustCompile(`(?i)version/(\d+\.\d+)`),
  173. },
  174. // Other/Legacy Browsers
  175. {
  176. name: "NetFront",
  177. pattern: regexp.MustCompile(`(?i)netfront`),
  178. version: regexp.MustCompile(`(?i)netfront/(\d+\.\d+)`),
  179. },
  180. {
  181. name: "Konqueror",
  182. pattern: regexp.MustCompile(`(?i)konqueror`),
  183. version: regexp.MustCompile(`(?i)konqueror/(\d+\.\d+)`),
  184. },
  185. }
  186. }
  187. func initOSPatterns() []osPattern {
  188. return []osPattern{
  189. // Mobile OS (highest priority)
  190. {
  191. name: "iOS",
  192. pattern: regexp.MustCompile(`(?i)iPhone OS|OS (\d+_\d+)|iPad; OS|iPod.*OS|iPhone.*OS`),
  193. version: regexp.MustCompile(`(?i)OS (\d+[_\d]*)`),
  194. },
  195. {
  196. name: "Android",
  197. pattern: regexp.MustCompile(`(?i)android`),
  198. version: regexp.MustCompile(`(?i)android (\d+\.?\d*\.?\d*)`),
  199. },
  200. // Desktop OS
  201. {
  202. name: "Windows",
  203. pattern: regexp.MustCompile(`(?i)windows`),
  204. version: regexp.MustCompile(`(?i)windows nt (\d+\.?\d*)`),
  205. },
  206. {
  207. name: "macOS",
  208. pattern: regexp.MustCompile(`(?i)mac os x|macintosh|intel mac`),
  209. version: regexp.MustCompile(`(?i)mac os x (\d+[_\d]*)`),
  210. },
  211. // Linux Distributions
  212. {
  213. name: "Ubuntu",
  214. pattern: regexp.MustCompile(`(?i)ubuntu`),
  215. version: regexp.MustCompile(`(?i)ubuntu[\/\s]*(\d+\.?\d*\.?\d*)`),
  216. },
  217. {
  218. name: "CentOS",
  219. pattern: regexp.MustCompile(`(?i)centos`),
  220. version: regexp.MustCompile(`(?i)centos[\/\s]*(\d+\.?\d*\.?\d*)`),
  221. },
  222. {
  223. name: "Red Hat",
  224. pattern: regexp.MustCompile(`(?i)red.*hat|rhel`),
  225. version: regexp.MustCompile(`(?i)red.*hat[\/\s]*(\d+\.?\d*\.?\d*)|rhel[\/\s]*(\d+\.?\d*\.?\d*)`),
  226. },
  227. {
  228. name: "Debian",
  229. pattern: regexp.MustCompile(`(?i)debian`),
  230. version: regexp.MustCompile(`(?i)debian[\/\s]*(\d+\.?\d*\.?\d*)`),
  231. },
  232. {
  233. name: "Fedora",
  234. pattern: regexp.MustCompile(`(?i)fedora`),
  235. version: regexp.MustCompile(`(?i)fedora[\/\s]*(\d+\.?\d*\.?\d*)`),
  236. },
  237. {
  238. name: "SUSE",
  239. pattern: regexp.MustCompile(`(?i)suse|opensuse`),
  240. version: regexp.MustCompile(`(?i)suse[\/\s]*(\d+\.?\d*\.?\d*)|opensuse[\/\s]*(\d+\.?\d*\.?\d*)`),
  241. },
  242. // Other Unix-like
  243. {
  244. name: "FreeBSD",
  245. pattern: regexp.MustCompile(`(?i)freebsd`),
  246. version: regexp.MustCompile(`(?i)freebsd (\d+\.?\d*\.?\d*)`),
  247. },
  248. {
  249. name: "OpenBSD",
  250. pattern: regexp.MustCompile(`(?i)openbsd`),
  251. version: regexp.MustCompile(`(?i)openbsd (\d+\.?\d*\.?\d*)`),
  252. },
  253. {
  254. name: "NetBSD",
  255. pattern: regexp.MustCompile(`(?i)netbsd`),
  256. version: regexp.MustCompile(`(?i)netbsd (\d+\.?\d*\.?\d*)`),
  257. },
  258. // Generic Linux fallback
  259. {
  260. name: "Linux",
  261. pattern: regexp.MustCompile(`(?i)linux|x11`),
  262. version: nil,
  263. },
  264. // Other/Legacy OS
  265. {
  266. name: "Chrome OS",
  267. pattern: regexp.MustCompile(`(?i)cros`),
  268. version: regexp.MustCompile(`(?i)cros (\d+\.?\d*\.?\d*)`),
  269. },
  270. {
  271. name: "Windows Phone",
  272. pattern: regexp.MustCompile(`(?i)windows phone`),
  273. version: regexp.MustCompile(`(?i)windows phone (\d+\.?\d*\.?\d*)`),
  274. },
  275. {
  276. name: "BlackBerry",
  277. pattern: regexp.MustCompile(`(?i)blackberry|bb10`),
  278. version: regexp.MustCompile(`(?i)blackberry[\/\s]*(\d+\.?\d*\.?\d*)|bb10[\/\s]*(\d+\.?\d*\.?\d*)`),
  279. },
  280. {
  281. name: "Symbian",
  282. pattern: regexp.MustCompile(`(?i)symbian|s60`),
  283. version: regexp.MustCompile(`(?i)symbian[\/\s]*(\d+\.?\d*\.?\d*)|s60[\/\s]*(\d+\.?\d*\.?\d*)`),
  284. },
  285. }
  286. }
  287. func initDevicePatterns() []devicePattern {
  288. return []devicePattern{
  289. // Bots (highest priority)
  290. {
  291. name: "Bot",
  292. pattern: regexp.MustCompile(`(?i)bot|crawler|spider|crawl|slurp|sohu-search|lycos|robozilla|googlebot|bingbot|facebookexternalhit|twitterbot|whatsapp|telegrambot|applebot|linkedinbot|pinterest|yandexbot|baiduspider|360spider|sogou|bytedance|scraper`),
  293. },
  294. // Apple Devices (specific models first)
  295. {
  296. name: "iPhone",
  297. pattern: regexp.MustCompile(`(?i)iphone`),
  298. },
  299. {
  300. name: "iPad",
  301. pattern: regexp.MustCompile(`(?i)ipad`),
  302. },
  303. {
  304. name: "iPod",
  305. pattern: regexp.MustCompile(`(?i)ipod`),
  306. },
  307. {
  308. name: "Apple Watch",
  309. pattern: regexp.MustCompile(`(?i)watch.*os`),
  310. },
  311. {
  312. name: "Apple TV",
  313. pattern: regexp.MustCompile(`(?i)apple.*tv`),
  314. },
  315. // Gaming Consoles
  316. {
  317. name: "PlayStation",
  318. pattern: regexp.MustCompile(`(?i)playstation|ps[345]|psvita`),
  319. },
  320. {
  321. name: "Xbox",
  322. pattern: regexp.MustCompile(`(?i)xbox`),
  323. },
  324. {
  325. name: "Nintendo",
  326. pattern: regexp.MustCompile(`(?i)nintendo|wii|3ds|switch`),
  327. },
  328. // Smart TVs and Streaming Devices
  329. {
  330. name: "Smart TV",
  331. pattern: regexp.MustCompile(`(?i)smart.*tv|smarttv|hbbtv|netcast|roku|webos|tizen|android.*tv`),
  332. },
  333. {
  334. name: "Chromecast",
  335. pattern: regexp.MustCompile(`(?i)chromecast`),
  336. },
  337. // Tablets (before Mobile for proper detection)
  338. {
  339. name: "Tablet",
  340. pattern: regexp.MustCompile(`(?i)tablet|ipad|kindle|nook|playbook|touchpad|xoom|sch-i800|gt-p1000|sgh-t849|shw-m180s|a1_07|bntv250a|mid7015|mid7012`),
  341. },
  342. // Mobile Phones (Android and others)
  343. {
  344. name: "Mobile",
  345. pattern: regexp.MustCompile(`(?i)mobile|phone|iphone|android.*mobile|blackberry|bb10|windows phone|iemobile|palm|webos|symbian|maemo|fennec|minimo|pda|pocket|psp|smartphone|mobileexplorer|htc|samsung|lg|motorola|sony|nokia|huawei|xiaomi|oppo|vivo|oneplus`),
  346. },
  347. // Wearables
  348. {
  349. name: "Wearable",
  350. pattern: regexp.MustCompile(`(?i)watch|wearable|fitbit|gear`),
  351. },
  352. // IoT and Other Devices
  353. {
  354. name: "IoT Device",
  355. pattern: regexp.MustCompile(`(?i)alexa|echo|iot|raspberry|arduino`),
  356. },
  357. // E-readers
  358. {
  359. name: "E-Reader",
  360. pattern: regexp.MustCompile(`(?i)kindle|nook|kobo|pocketbook`),
  361. },
  362. // Default fallback (must be last)
  363. {
  364. name: "Desktop",
  365. pattern: regexp.MustCompile(`.*`),
  366. },
  367. }
  368. }
  369. // Parse parses a user agent string and returns detailed information
  370. func (p *SimpleUserAgentParser) Parse(userAgent string) UserAgentInfo {
  371. if userAgent == "" || userAgent == "-" {
  372. return UserAgentInfo{
  373. Browser: "Unknown",
  374. BrowserVer: "",
  375. OS: "Unknown",
  376. OSVersion: "",
  377. DeviceType: "Desktop",
  378. }
  379. }
  380. info := UserAgentInfo{
  381. Browser: "Unknown",
  382. BrowserVer: "",
  383. OS: "Unknown",
  384. OSVersion: "",
  385. DeviceType: "Desktop",
  386. }
  387. // Parse browser information
  388. for _, bp := range p.browserPatterns {
  389. if bp.pattern.MatchString(userAgent) {
  390. info.Browser = bp.name
  391. if bp.version != nil {
  392. if matches := bp.version.FindStringSubmatch(userAgent); len(matches) > 1 {
  393. info.BrowserVer = matches[1]
  394. }
  395. }
  396. break
  397. }
  398. }
  399. // Parse OS information
  400. for _, op := range p.osPatterns {
  401. if op.pattern.MatchString(userAgent) {
  402. info.OS = op.name
  403. if op.version != nil {
  404. if matches := op.version.FindStringSubmatch(userAgent); len(matches) > 1 {
  405. version := matches[1]
  406. // Clean up version string
  407. version = strings.ReplaceAll(version, "_", ".")
  408. info.OSVersion = version
  409. }
  410. }
  411. break
  412. }
  413. }
  414. // Parse device type with improved logic
  415. for _, dp := range p.devicePatterns {
  416. if dp.pattern.MatchString(userAgent) {
  417. info.DeviceType = dp.name
  418. if dp.name != "Desktop" { // Don't break on Desktop (fallback)
  419. break
  420. }
  421. }
  422. }
  423. // Post-processing to fix common issues
  424. info = p.postProcessInfo(info, userAgent)
  425. return info
  426. }
  427. // postProcessInfo applies post-processing rules to improve detection accuracy
  428. func (p *SimpleUserAgentParser) postProcessInfo(info UserAgentInfo, userAgent string) UserAgentInfo {
  429. userAgentLower := strings.ToLower(userAgent)
  430. // Fix version extraction for some browsers
  431. if info.BrowserVer == "" {
  432. info.BrowserVer = p.extractVersion(info.Browser, userAgent)
  433. }
  434. // Special handling for mobile vs tablet detection
  435. if info.DeviceType == "Mobile" {
  436. if strings.Contains(userAgentLower, "ipad") ||
  437. strings.Contains(userAgentLower, "tablet") ||
  438. (strings.Contains(userAgentLower, "android") && !strings.Contains(userAgentLower, "mobile")) {
  439. info.DeviceType = "Tablet"
  440. }
  441. }
  442. // Fix Android tablet detection when detected as Desktop
  443. if info.DeviceType == "Desktop" && strings.Contains(userAgentLower, "android") && !strings.Contains(userAgentLower, "mobile") {
  444. info.DeviceType = "Tablet"
  445. }
  446. // Clean up OS versions
  447. if info.OSVersion != "" {
  448. info.OSVersion = p.cleanVersion(info.OSVersion)
  449. }
  450. // Fix browser names for specific cases
  451. info.Browser = p.fixBrowserName(info.Browser, userAgent)
  452. return info
  453. }
  454. // extractVersion extracts version from user agent for specific browsers
  455. func (p *SimpleUserAgentParser) extractVersion(browser, userAgent string) string {
  456. switch browser {
  457. case "WeChat":
  458. if matches := regexp.MustCompile(`(?i)micromessenger/(\d+\.\d+\.\d+)`).FindStringSubmatch(userAgent); len(matches) > 1 {
  459. return matches[1]
  460. }
  461. case "QQ":
  462. if matches := regexp.MustCompile(`(?i)qq/(\d+\.\d+\.\d+)`).FindStringSubmatch(userAgent); len(matches) > 1 {
  463. return matches[1]
  464. }
  465. case "Alipay":
  466. if matches := regexp.MustCompile(`(?i)alipayclient/(\d+\.\d+\.\d+)`).FindStringSubmatch(userAgent); len(matches) > 1 {
  467. return matches[1]
  468. }
  469. }
  470. return ""
  471. }
  472. // cleanVersion cleans up version strings
  473. func (p *SimpleUserAgentParser) cleanVersion(version string) string {
  474. // Replace underscores with dots for iOS versions
  475. version = strings.ReplaceAll(version, "_", ".")
  476. // Only trim trailing .0 patterns, not individual zeros
  477. for strings.HasSuffix(version, ".0") {
  478. version = strings.TrimSuffix(version, ".0")
  479. }
  480. return version
  481. }
  482. // fixBrowserName applies corrections to browser names
  483. func (p *SimpleUserAgentParser) fixBrowserName(browser, userAgent string) string {
  484. userAgentLower := strings.ToLower(userAgent)
  485. // Distinguish between different Chrome-based browsers
  486. if browser == "Chrome" {
  487. if strings.Contains(userAgentLower, "edg/") {
  488. return "Edge"
  489. }
  490. if strings.Contains(userAgentLower, "opr/") {
  491. return "Opera"
  492. }
  493. if strings.Contains(userAgentLower, "samsungbrowser") {
  494. return "Samsung Browser"
  495. }
  496. }
  497. return browser
  498. }
  499. // IsBot returns true if the user agent appears to be a bot/crawler
  500. func (p *SimpleUserAgentParser) IsBot(userAgent string) bool {
  501. info := p.Parse(userAgent)
  502. return info.Browser == "Bot" || info.DeviceType == "Bot"
  503. }
  504. // IsMobile returns true if the user agent appears to be from a mobile device
  505. func (p *SimpleUserAgentParser) IsMobile(userAgent string) bool {
  506. info := p.Parse(userAgent)
  507. return info.DeviceType == "Mobile" || info.DeviceType == "iPhone"
  508. }
  509. // IsTablet returns true if the user agent appears to be from a tablet device
  510. func (p *SimpleUserAgentParser) IsTablet(userAgent string) bool {
  511. info := p.Parse(userAgent)
  512. return info.DeviceType == "Tablet" || info.DeviceType == "iPad"
  513. }
  514. // GetSimpleDeviceType returns a simplified device type (Mobile, Tablet, Desktop, Bot)
  515. func (p *SimpleUserAgentParser) GetSimpleDeviceType(userAgent string) string {
  516. info := p.Parse(userAgent)
  517. switch info.DeviceType {
  518. case "iPhone", "Mobile":
  519. return "Mobile"
  520. case "iPad", "Tablet":
  521. return "Tablet"
  522. case "Bot":
  523. return "Bot"
  524. default:
  525. return "Desktop"
  526. }
  527. }
  528. // CachedUserAgentParser provides caching for parsed user agents
  529. type CachedUserAgentParser struct {
  530. parser UserAgentParser
  531. cache map[string]UserAgentInfo
  532. maxSize int
  533. mu sync.RWMutex
  534. }
  535. // NewCachedUserAgentParser creates a cached user agent parser
  536. func NewCachedUserAgentParser(parser UserAgentParser, maxSize int) *CachedUserAgentParser {
  537. if maxSize <= 0 {
  538. maxSize = 1000
  539. }
  540. return &CachedUserAgentParser{
  541. parser: parser,
  542. cache: make(map[string]UserAgentInfo),
  543. maxSize: maxSize,
  544. }
  545. }
  546. // Parse parses a user agent string with caching
  547. func (p *CachedUserAgentParser) Parse(userAgent string) UserAgentInfo {
  548. // Try to get from cache with read lock
  549. p.mu.RLock()
  550. if info, exists := p.cache[userAgent]; exists {
  551. p.mu.RUnlock()
  552. return info
  553. }
  554. p.mu.RUnlock()
  555. // Parse and cache with write lock
  556. p.mu.Lock()
  557. defer p.mu.Unlock()
  558. // Double-check after acquiring write lock
  559. if info, exists := p.cache[userAgent]; exists {
  560. return info
  561. }
  562. // If cache is full, clear it (simple eviction strategy)
  563. if len(p.cache) >= p.maxSize {
  564. p.cache = make(map[string]UserAgentInfo)
  565. }
  566. info := p.parser.Parse(userAgent)
  567. p.cache[userAgent] = info
  568. return info
  569. }
  570. // GetCacheStats returns cache statistics
  571. func (p *CachedUserAgentParser) GetCacheStats() (size int, maxSize int) {
  572. p.mu.RLock()
  573. defer p.mu.RUnlock()
  574. return len(p.cache), p.maxSize
  575. }
  576. // ClearCache clears the parser cache
  577. func (p *CachedUserAgentParser) ClearCache() {
  578. p.mu.Lock()
  579. defer p.mu.Unlock()
  580. p.cache = make(map[string]UserAgentInfo)
  581. }