1
2
3
4
5 package template
6
7 import (
8 "bytes"
9 "fmt"
10 "strings"
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 func urlFilter(args ...any) string {
36 s, t := stringify(args...)
37 if t == contentTypeURL {
38 return s
39 }
40 if !isSafeURL(s) {
41 return "#" + filterFailsafe
42 }
43 return s
44 }
45
46
47
48 func isSafeURL(s string) bool {
49 if protocol, _, ok := strings.Cut(s, ":"); ok && !strings.Contains(protocol, "/") {
50 if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") {
51 return false
52 }
53 }
54 return true
55 }
56
57
58
59 func urlEscaper(args ...any) string {
60 return urlProcessor(false, args...)
61 }
62
63
64
65
66
67
68 func urlNormalizer(args ...any) string {
69 return urlProcessor(true, args...)
70 }
71
72
73
74 func urlProcessor(norm bool, args ...any) string {
75 s, t := stringify(args...)
76 if t == contentTypeURL {
77 norm = true
78 }
79 var b bytes.Buffer
80 if processURLOnto(s, norm, &b) {
81 return b.String()
82 }
83 return s
84 }
85
86
87
88 func processURLOnto(s string, norm bool, b *bytes.Buffer) bool {
89 b.Grow(len(s) + 16)
90 written := 0
91
92
93
94
95
96
97 for i, n := 0, len(s); i < n; i++ {
98 c := s[i]
99 switch c {
100
101
102
103
104
105
106 case '!', '#', '$', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']':
107 if norm {
108 continue
109 }
110
111
112
113
114
115 case '-', '.', '_', '~':
116 continue
117 case '%':
118
119 if norm && i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) {
120 continue
121 }
122 default:
123
124 if 'a' <= c && c <= 'z' {
125 continue
126 }
127 if 'A' <= c && c <= 'Z' {
128 continue
129 }
130 if '0' <= c && c <= '9' {
131 continue
132 }
133 }
134 b.WriteString(s[written:i])
135 fmt.Fprintf(b, "%%%02x", c)
136 written = i + 1
137 }
138 b.WriteString(s[written:])
139 return written != 0
140 }
141
142
143
144 func srcsetFilterAndEscaper(args ...any) string {
145 s, t := stringify(args...)
146 switch t {
147 case contentTypeSrcset:
148 return s
149 case contentTypeURL:
150
151
152 var b bytes.Buffer
153 if processURLOnto(s, true, &b) {
154 s = b.String()
155 }
156
157 return strings.ReplaceAll(s, ",", "%2c")
158 }
159
160 var b bytes.Buffer
161 written := 0
162 for i := 0; i < len(s); i++ {
163 if s[i] == ',' {
164 filterSrcsetElement(s, written, i, &b)
165 b.WriteString(",")
166 written = i + 1
167 }
168 }
169 filterSrcsetElement(s, written, len(s), &b)
170 return b.String()
171 }
172
173
174 const htmlSpaceAndASCIIAlnumBytes = "\x00\x36\x00\x00\x01\x00\xff\x03\xfe\xff\xff\x07\xfe\xff\xff\x07"
175
176
177
178 func isHTMLSpace(c byte) bool {
179 return (c <= 0x20) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
180 }
181
182 func isHTMLSpaceOrASCIIAlnum(c byte) bool {
183 return (c < 0x80) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
184 }
185
186 func filterSrcsetElement(s string, left int, right int, b *bytes.Buffer) {
187 start := left
188 for start < right && isHTMLSpace(s[start]) {
189 start++
190 }
191 end := right
192 for i := start; i < right; i++ {
193 if isHTMLSpace(s[i]) {
194 end = i
195 break
196 }
197 }
198 if url := s[start:end]; isSafeURL(url) {
199
200
201 metadataOk := true
202 for i := end; i < right; i++ {
203 if !isHTMLSpaceOrASCIIAlnum(s[i]) {
204 metadataOk = false
205 break
206 }
207 }
208 if metadataOk {
209 b.WriteString(s[left:start])
210 processURLOnto(url, true, b)
211 b.WriteString(s[end:right])
212 return
213 }
214 }
215 b.WriteString("#")
216 b.WriteString(filterFailsafe)
217 }
218
View as plain text