@@ -5,15 +5,28 @@ import (
55 "math"
66 "regexp"
77 "strconv"
8+ "strings"
89 "time"
910)
1011
12+ // regexChars contains the list of regex metacharacters, excluding +,
13+ // which is valid in a key
14+ const regexChars = `^$.|?*-[]{}()\`
15+
16+ // regexWillNeverMatch is a shared error message that the regex will never match
17+ const regexWillNeverMatch = "The regular expression will never match"
18+
1119// IsValidSearch checks the search does not contain any invalid characters
1220func IsValidSearch (s string ) bool {
1321 var r = regexp .MustCompile (`[^a-zA-Z0-9\/\+]` )
1422 return ! r .MatchString (s )
1523}
1624
25+ // InvalidSearchMsg returns the error message the search term contains invalid characters
26+ func InvalidSearchMsg (s string ) string {
27+ return fmt .Sprintf ("\n \" %s\" contains invalid characters\n Valid characters include letters [a-z], numbers [0-9], + and /" , s )
28+ }
29+
1730// HumanizeDuration returns a human-readable output of time.Duration
1831func HumanizeDuration (duration time.Duration ) string {
1932 // more than duration can handle
@@ -87,3 +100,111 @@ func NumberFormat(n int64) string {
87100 }
88101 }
89102}
103+
104+ // IsRegex returns true if any regex metacharacters (except +) are in the search term
105+ func IsRegex (s string ) bool {
106+ return strings .ContainsAny (s , regexChars )
107+ }
108+
109+ // invalidRegexMsg returns an error message how the regex is invalid
110+ func invalidRegexMsg (s string , errmsg string ) string {
111+ return fmt .Sprintf ("\n \" %s\" is an invalid regular expression\n %s" , s , errmsg )
112+ }
113+
114+ // IsValidRegex checks the regex has any chance of matching a key
115+ func IsValidRegex (s string ) string {
116+ // A consise guide on golang's regex syntax is at
117+ // https://pkg.go.dev/regexp/syntax
118+
119+ stripped := removeMetacharacters (s )
120+ if ! IsValidSearch (stripped ) {
121+ return InvalidSearchMsg (s )
122+ }
123+
124+ // Expressions with '^' character
125+ re := regexp .MustCompile (`.\^` )
126+ if re .MatchString (s ) {
127+ return invalidRegexMsg (s , "The '^' character must appear at the beginning of the search term" )
128+ }
129+
130+ // Expressions with '$' character
131+ re = regexp .MustCompile (`\$.` )
132+ if re .MatchString (s ) {
133+ return invalidRegexMsg (s , "The '$' character must appear at the end of the search term" )
134+ }
135+ re = regexp .MustCompile (`[^=]\$` )
136+ if re .MatchString (s ) {
137+ return invalidRegexMsg (s , "A search at the end of the string must contain an '=' character, as all keys end with an `=`" )
138+ }
139+ re = regexp .MustCompile (`=[^$]` )
140+ if re .MatchString (s ) {
141+ return invalidRegexMsg (s , "The '=' character can only appear at the end of a key" )
142+ }
143+ // The command:
144+ // wireguard-vanity-keygen -l 1000 . | grep private | cut -c 105- | sort -u | tr -d "=" | tr -d "\n"
145+ // outputs:
146+ // 048AEIMQUYcgkosw
147+ re = regexp .MustCompile (`[^048AEIMQUYcgkosw]=\$` )
148+ if re .MatchString (s ) {
149+ return invalidRegexMsg (s , regexWillNeverMatch )
150+ }
151+
152+ // Expressions with backslashes:
153+
154+ // A regex of just a backslash and a single character will never match
155+ re = regexp .MustCompile (`^\\.$` )
156+ if re .MatchString (s ) {
157+ return invalidRegexMsg (s , regexWillNeverMatch )
158+ }
159+
160+ // Control characters and many octal values will meter match, disallow them all
161+ re = regexp .MustCompile (`\\[aftnrxswWpP0-7]` )
162+ if re .MatchString (s ) {
163+ return invalidRegexMsg (s , regexWillNeverMatch )
164+ }
165+
166+ // Disallow backslashes followed by any non-alnum or + character
167+ re = regexp .MustCompile (`\\[^A-Za-z0-9+]` )
168+ if re .MatchString (s ) {
169+ return invalidRegexMsg (s , regexWillNeverMatch )
170+ }
171+
172+ // Expressions with character classes: [[:alnum:]], etc.
173+
174+ // [[:blank:]], [[:cntrl:]], [[:punct:]] and [[:space:]] will never match
175+ re = regexp .MustCompile (`\[\[:(blank|cntrl|punct|space):\]\]` )
176+ if re .MatchString (s ) {
177+ return invalidRegexMsg (s , regexWillNeverMatch )
178+ }
179+
180+ // [[^:ascii:]], [[^:graph:]], [[^:print:]] will never match
181+ re = regexp .MustCompile (`\[\[\^:(ascii|graph|print):\]\]` )
182+ if re .MatchString (s ) {
183+ return invalidRegexMsg (s , regexWillNeverMatch )
184+ }
185+
186+ return ""
187+ }
188+
189+ // removeMetacharacters removes regex metacharacters from the string
190+ func removeMetacharacters (s string ) string {
191+ // This logic isn't needed anymore, as we don't attempt to calculate the probability of regular expressions
192+ // // remove (?i) from beginning of string
193+ // re := regexp.MustCompile(`^\([^)]*\)`)
194+ // s = re.ReplaceAllLiteralString(s, "")
195+ // // replace [a-b]+ with x
196+ // re = regexp.MustCompile(`\[[^]]*\]\+?`)
197+ // s = re.ReplaceAllLiteralString(s, "x")
198+ // // strip all {n}
199+ // re = regexp.MustCompile(`\{[^}]+\}`)
200+ // s = re.ReplaceAllLiteralString(s, "")
201+ // // replace = with x
202+ // re = regexp.MustCompile(`=`)
203+ // s = re.ReplaceAllLiteralString(s, "x")
204+
205+ // strip out remaining regexp metacharacters
206+ for _ , rune1 := range []rune (regexChars ) {
207+ s = strings .ReplaceAll (s , string (rune1 ), "" )
208+ }
209+ return s
210+ }
0 commit comments