@@ -2,66 +2,144 @@ package org.uaparser.scala
22
33import java .util .regex .{Matcher , Pattern }
44
5- import scala .util . control . Exception . allCatch
5+ import scala .collection . mutable . ListBuffer
66
77import org .uaparser .scala .MatcherOps .MatcherImprovements
8- import org .uaparser .scala .OSPattern .{replaceBackreference , replacementBack1 }
98
10- private [scala] case class OSPattern (
9+ private [scala] final case class OSPattern (
1110 pattern : Pattern ,
12- osReplacement : Option [String ],
13- v1Replacement : Option [String ],
14- v2Replacement : Option [String ],
15- v3Replacement : Option [String ],
16- v4Replacement : Option [String ]
11+ osReplacement : Option [OSPattern . FamilyReplacement ],
12+ v1Replacement : Option [OSPattern . VersionReplacement ],
13+ v2Replacement : Option [OSPattern . VersionReplacement ],
14+ v3Replacement : Option [OSPattern . VersionReplacement ],
15+ v4Replacement : Option [OSPattern . VersionReplacement ]
1716) {
17+
1818 def process (agent : String ): Option [OS ] = {
19- val matcher = pattern.matcher(agent)
20- if (! matcher .find()) None
19+ val m = pattern.matcher(agent)
20+ if (! m .find()) None
2121 else {
22- osReplacement
23- .map(replacementBack1(matcher))
24- .orElse(matcher.groupAt(1 ))
25- .map { family =>
26- val major = v1Replacement.flatMap(replaceBackreference(matcher)).orElse(matcher.groupAt(2 ))
27- val minor = v2Replacement.flatMap(replaceBackreference(matcher)).orElse(matcher.groupAt(3 ))
28- val patch = v3Replacement.flatMap(replaceBackreference(matcher)).orElse(matcher.groupAt(4 ))
29- val patchMinor = v4Replacement.flatMap(replaceBackreference(matcher)).orElse(matcher.groupAt(5 ))
30- OS (family, major, minor, patch, patchMinor)
22+ val familyOpt : Option [String ] =
23+ osReplacement match {
24+ case Some (rep) => Some (rep.render(agent, m))
25+ case None => m.groupAt(1 )
3126 }
27+
28+ familyOpt.map { family =>
29+ val major = OSPattern .resolveVersion(v1Replacement, m, fallbackGroup = 2 )
30+ val minor = OSPattern .resolveVersion(v2Replacement, m, fallbackGroup = 3 )
31+ val patch = OSPattern .resolveVersion(v3Replacement, m, fallbackGroup = 4 )
32+ val patchMinor = OSPattern .resolveVersion(v4Replacement, m, fallbackGroup = 5 )
33+ OS (family, major, minor, patch, patchMinor)
34+ }
3235 }
3336 }
3437}
3538
3639private object OSPattern {
37- private [this ] val quotedBack1 : Pattern = Pattern .compile(s " ( ${Pattern .quote(" $1" )}) " )
38-
39- private [this ] def getBackreferenceGroup (replacement : String ): Option [Int ] =
40- for {
41- ref <- Option (replacement).filter(_.contains(" $" ))
42- groupOpt = allCatch.opt(ref.substring(1 ).toInt)
43- group <- groupOpt
44- } yield group
45-
46- private def replacementBack1 (matcher : Matcher )(replacement : String ): String =
47- if (matcher.groupCount() >= 1 ) {
48- quotedBack1.matcher(replacement).replaceAll(matcher.group(1 ))
49- } else replacement
50-
51- private def replaceBackreference (matcher : Matcher )(replacement : String ): Option [String ] =
52- getBackreferenceGroup(replacement) match {
53- case Some (group) => matcher.groupAt(group)
54- case None => Some (replacement)
40+
41+ private val Dollar1 = " $1"
42+
43+ sealed trait FamilyReplacement {
44+ def render (agent : String , m : Matcher ): String
45+ }
46+
47+ private final case class FamilyLiteral (value : String ) extends FamilyReplacement {
48+ override def render (agent : String , m : Matcher ): String = value
49+ }
50+ private final case class FamilyWithGroup1 (parts : Array [String ]) extends FamilyReplacement {
51+
52+ // We insert group 1 between each part.
53+ override def render (agent : String , m : Matcher ): String = {
54+ val stringBuilder = new java.lang.StringBuilder ()
55+ val hasGroup1 = m.groupCount() >= 1 && m.start(1 ) >= 0
56+ val partsLength = parts.length
57+
58+ var i = 0
59+ parts.foreach { part =>
60+ stringBuilder.append(part)
61+ if (i < partsLength - 1 ) {
62+ if (hasGroup1) stringBuilder.append(agent, m.start(1 ), m.end(1 ))
63+ }
64+ i += 1
65+ }
66+ stringBuilder.toString
67+ }
68+ }
69+
70+ private def compileFamilyReplacement (replacementDef : String ): FamilyReplacement = {
71+ val first = replacementDef.indexOf(Dollar1 )
72+ if (first < 0 ) FamilyLiteral (replacementDef)
73+ else {
74+ // Split by "$1". Keep empty segments.
75+ val buf = ListBuffer .empty[String ]
76+ var from = 0
77+ var idx = first
78+ while (idx >= 0 ) {
79+ buf += replacementDef.substring(from, idx)
80+ from = idx + Dollar1 .length
81+ idx = replacementDef.indexOf(Dollar1 , from)
82+ }
83+ buf += replacementDef.substring(from)
84+ FamilyWithGroup1 (buf.toArray)
5585 }
86+ }
5687
57- def fromMap (m : Map [String , String ]): Option [OSPattern ] = m.get(" regex" ).map { r =>
58- OSPattern (
59- Pattern .compile(r),
60- m.get(" os_replacement" ),
61- m.get(" os_v1_replacement" ),
62- m.get(" os_v2_replacement" ),
63- m.get(" os_v3_replacement" ),
64- m.get(" os_v4_replacement" )
65- )
88+ sealed trait VersionReplacement
89+ private final case class VersionLiteral (value : String ) extends VersionReplacement
90+ private final case class VersionGroupRef (group : Int ) extends VersionReplacement
91+
92+ private def compileVersionReplacement (replacementDef : String ): VersionReplacement = {
93+ // Treat only the whole string "$<digits>" as a group reference, otherwise it will be treated as a literal.
94+ if (replacementDef != null && replacementDef.length >= 2 && replacementDef.charAt(0 ) == '$' ) {
95+ var i = 1
96+ var n = 0
97+ var hasDigits = false
98+ while (i < replacementDef.length) {
99+ val ch = replacementDef.charAt(i)
100+ if (ch >= '0' && ch <= '9' ) {
101+ hasDigits = true
102+ n = n * 10 + (ch - '0' )
103+ i += 1
104+ } else {
105+ return VersionLiteral (replacementDef)
106+ }
107+ }
108+ if (hasDigits && n > 0 ) VersionGroupRef (n) else VersionLiteral (replacementDef)
109+ } else VersionLiteral (replacementDef)
66110 }
111+
112+ private def evalVersion (rep : VersionReplacement , m : Matcher ): Option [String ] =
113+ rep match {
114+ case VersionLiteral (v) => if (v == null || v.isEmpty) None else Some (v)
115+ case VersionGroupRef (gr) => m.groupAt(gr)
116+ }
117+
118+ // - replacement is a backref and missing, fall back to the default captured group
119+ // - replacement is a literal, use it (unless empty)
120+ // - no replacement provided, use fallback captured group
121+ private def resolveVersion (repOpt : Option [VersionReplacement ], matcher : Matcher , fallbackGroup : Int ): Option [String ] =
122+ repOpt match {
123+ case Some (VersionGroupRef (gr)) => matcher.groupAt(gr).orElse(matcher.groupAt(fallbackGroup))
124+ case Some (litOrOther) => evalVersion(litOrOther, matcher).orElse(matcher.groupAt(fallbackGroup))
125+ case None => matcher.groupAt(fallbackGroup)
126+ }
127+
128+ def fromMap (m : Map [String , String ]): Option [OSPattern ] =
129+ m.get(" regex" ).map { r =>
130+ val osRep = m.get(" os_replacement" ).map(compileFamilyReplacement)
131+ val v1 = m.get(" os_v1_replacement" ).map(compileVersionReplacement)
132+ val v2 = m.get(" os_v2_replacement" ).map(compileVersionReplacement)
133+ val v3 = m.get(" os_v3_replacement" ).map(compileVersionReplacement)
134+ val v4 = m.get(" os_v4_replacement" ).map(compileVersionReplacement)
135+
136+ OSPattern (
137+ pattern = Pattern .compile(r),
138+ osReplacement = osRep,
139+ v1Replacement = v1,
140+ v2Replacement = v2,
141+ v3Replacement = v3,
142+ v4Replacement = v4
143+ )
144+ }
67145}
0 commit comments