-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnormalize.go
More file actions
147 lines (133 loc) · 3.88 KB
/
Copy pathnormalize.go
File metadata and controls
147 lines (133 loc) · 3.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package eql
import (
"strings"
)
// NormalizeQuery cleans up EQL text that was pasted from documents, extracted
// from JSON/YAML/TOML rule files, or otherwise mangled in transit, without
// changing the meaning of well-formed queries. ExtractConditions applies it
// automatically; Parse does not.
func NormalizeQuery(query string) string {
q := query
// Strip UTF-8 BOM.
q = strings.TrimPrefix(q, "\ufeff")
// Replace typographic characters that word processors substitute. These
// are done globally: curly quotes inside legitimate string values are
// far rarer than curly quotes produced by pasting a whole query through
// a rich-text editor.
replacer := strings.NewReplacer(
"\u201c", `"`, "\u201d", `"`, "\u201e", `"`, "\u201f", `"`, // curly double quotes
"\u2018", `'`, "\u2019", `'`, "\u201a", `'`, "\u201b", `'`, // curly single quotes
"\u00ab", `"`, "\u00bb", `"`, // guillemets
"\u2013", "-", "\u2014", "-", "\u2212", "-", // en/em dash, minus sign
"\u200b", "", "\u200c", "", "\u200d", "", "\ufeff", "", "\u2060", "", // zero-width chars
"\u00a0", " ", "\u202f", " ", "\u2007", " ", // non-breaking spaces
)
q = replacer.Replace(q)
// Strip markdown code fences: ```eql ... ``` or ``` ... ```.
q = stripCodeFences(q)
// Convert literal \n / \r\n / \t escape sequences outside string
// literals into real whitespace (queries extracted from JSON often
// carry them).
q = decodeEscapedWhitespace(q)
// Drop trailing semicolons (saved-query artifacts).
q = strings.TrimRight(q, " \t\r\n")
for strings.HasSuffix(q, ";") {
q = strings.TrimRight(strings.TrimSuffix(q, ";"), " \t\r\n")
}
return strings.TrimSpace(q)
}
func stripCodeFences(q string) string {
trimmed := strings.TrimSpace(q)
if !strings.HasPrefix(trimmed, "```") {
return q
}
lines := strings.Split(trimmed, "\n")
// Drop the opening fence line (with its optional language tag); trimmed is
// already known to start with ```. Since trimmed has no trailing blank
// lines, the closing fence is the last line when present.
lines = lines[1:]
if len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "```" {
lines = lines[:len(lines)-1]
}
return strings.Join(lines, "\n")
}
// decodeEscapedWhitespace converts literal backslash-n / backslash-r /
// backslash-t sequences that appear outside string literals into real
// whitespace. Inside strings they are legitimate escapes and left alone.
func decodeEscapedWhitespace(q string) string {
if !strings.Contains(q, `\n`) && !strings.Contains(q, `\r`) && !strings.Contains(q, `\t`) {
return q
}
var b strings.Builder
b.Grow(len(q))
i := 0
for i < len(q) {
c := q[i]
// Skip over comments verbatim.
if c == '/' && i+1 < len(q) && q[i+1] == '/' {
for i < len(q) && q[i] != '\n' {
b.WriteByte(q[i])
i++
}
continue
}
// Copy string literals verbatim (handles ", ', """ and raw forms).
if c == '"' || c == '\'' {
end := scanStringEnd(q, i)
b.WriteString(q[i:end])
i = end
continue
}
if c == '\\' && i+1 < len(q) {
switch q[i+1] {
case 'n':
b.WriteByte('\n')
i += 2
continue
case 'r':
b.WriteByte('\r')
i += 2
continue
case 't':
b.WriteByte('\t')
i += 2
continue
}
}
b.WriteByte(c)
i++
}
return b.String()
}
// scanStringEnd returns the index just past the string literal starting at
// position i (which must be a quote character).
func scanStringEnd(q string, i int) int {
quote := q[i]
// Triple-quoted raw string.
if quote == '"' && i+2 < len(q) && q[i+1] == '"' && q[i+2] == '"' {
j := i + 3
for j+2 < len(q) {
if q[j] == '"' && q[j+1] == '"' && q[j+2] == '"' {
return j + 3
}
j++
}
return len(q)
}
j := i + 1
for j < len(q) {
if q[j] == '\\' && j+1 < len(q) {
j += 2
continue
}
if q[j] == quote {
return j + 1
}
if q[j] == '\n' {
// EQL strings are single-line; treat as unterminated.
return j
}
j++
}
return len(q)
}