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