Codebase list golang-github-opennota-urlesc / 80fcd12
Imported Upstream version 0.0~git20150208.0.5fa9ff0 Anthony Fok 8 years ago
5 changed file(s) with 875 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 language: go
1
2 go:
3 - 1.4
4 - tip
5
6 install:
7 - go build .
8
9 script:
10 - go test -v
0 Copyright (c) 2012 The Go Authors. All rights reserved.
1
2 Redistribution and use in source and binary forms, with or without
3 modification, are permitted provided that the following conditions are
4 met:
5
6 * Redistributions of source code must retain the above copyright
7 notice, this list of conditions and the following disclaimer.
8 * Redistributions in binary form must reproduce the above
9 copyright notice, this list of conditions and the following disclaimer
10 in the documentation and/or other materials provided with the
11 distribution.
12 * Neither the name of Google Inc. nor the names of its
13 contributors may be used to endorse or promote products derived from
14 this software without specific prior written permission.
15
16 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 urlesc [![Build Status](https://travis-ci.org/opennota/urlesc.png?branch=master)](https://travis-ci.org/opennota/urlesc) [![GoDoc](http://godoc.org/github.com/opennota/urlesc?status.svg)](http://godoc.org/github.com/opennota/urlesc)
1 ======
2
3 Package urlesc implements query escaping as per RFC 3986.
4
5 It contains some parts of the net/url package, modified so as to allow
6 some reserved characters incorrectly escaped by net/url (see [issue 5684](https://github.com/golang/go/issues/5684)).
7
8 ## Install
9
10 go get github.com/opennota/urlesc
11
12 ## License
13
14 MIT
15
0 // Copyright 2009 The Go Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 // Package urlesc implements query escaping as per RFC 3986.
5 // It contains some parts of the net/url package, modified so as to allow
6 // some reserved characters incorrectly escaped by net/url.
7 // See https://github.com/golang/go/issues/5684
8 package urlesc
9
10 import (
11 "bytes"
12 "net/url"
13 "strings"
14 )
15
16 type encoding int
17
18 const (
19 encodePath encoding = 1 + iota
20 encodeUserPassword
21 encodeQueryComponent
22 encodeFragment
23 )
24
25 // Return true if the specified character should be escaped when
26 // appearing in a URL string, according to RFC 3986.
27 func shouldEscape(c byte, mode encoding) bool {
28 // §2.3 Unreserved characters (alphanum)
29 if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' {
30 return false
31 }
32
33 switch c {
34 case '-', '.', '_', '~': // §2.3 Unreserved characters (mark)
35 return false
36
37 // §2.2 Reserved characters (reserved)
38 case ':', '/', '?', '#', '[', ']', '@', // gen-delims
39 '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=': // sub-delims
40 // Different sections of the URL allow a few of
41 // the reserved characters to appear unescaped.
42 switch mode {
43 case encodePath: // §3.3
44 // The RFC allows sub-delims and : @.
45 // '/', '[' and ']' can be used to assign meaning to individual path
46 // segments. This package only manipulates the path as a whole,
47 // so we allow those as well. That leaves only ? and # to escape.
48 return c == '?' || c == '#'
49
50 case encodeUserPassword: // §3.2.1
51 // The RFC allows : and sub-delims in
52 // userinfo. The parsing of userinfo treats ':' as special so we must escape
53 // all the gen-delims.
54 return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' || c == '@'
55
56 case encodeQueryComponent: // §3.4
57 // The RFC allows / and ?.
58 return c != '/' && c != '?'
59
60 case encodeFragment: // §4.1
61 // The RFC text is silent but the grammar allows
62 // everything, so escape nothing.
63 return false
64 }
65 }
66
67 // Everything else must be escaped.
68 return true
69 }
70
71 // QueryEscape escapes the string so it can be safely placed
72 // inside a URL query.
73 func QueryEscape(s string) string {
74 return escape(s, encodeQueryComponent)
75 }
76
77 func escape(s string, mode encoding) string {
78 spaceCount, hexCount := 0, 0
79 for i := 0; i < len(s); i++ {
80 c := s[i]
81 if shouldEscape(c, mode) {
82 if c == ' ' && mode == encodeQueryComponent {
83 spaceCount++
84 } else {
85 hexCount++
86 }
87 }
88 }
89
90 if spaceCount == 0 && hexCount == 0 {
91 return s
92 }
93
94 t := make([]byte, len(s)+2*hexCount)
95 j := 0
96 for i := 0; i < len(s); i++ {
97 switch c := s[i]; {
98 case c == ' ' && mode == encodeQueryComponent:
99 t[j] = '+'
100 j++
101 case shouldEscape(c, mode):
102 t[j] = '%'
103 t[j+1] = "0123456789ABCDEF"[c>>4]
104 t[j+2] = "0123456789ABCDEF"[c&15]
105 j += 3
106 default:
107 t[j] = s[i]
108 j++
109 }
110 }
111 return string(t)
112 }
113
114 var uiReplacer = strings.NewReplacer(
115 "%21", "!",
116 "%27", "'",
117 "%28", "(",
118 "%29", ")",
119 "%2A", "*",
120 )
121
122 // unescapeUserinfo unescapes some characters that need not to be escaped as per RFC3986.
123 func unescapeUserinfo(s string) string {
124 return uiReplacer.Replace(s)
125 }
126
127 // Escape reassembles the URL into a valid URL string.
128 // The general form of the result is one of:
129 //
130 // scheme:opaque
131 // scheme://userinfo@host/path?query#fragment
132 //
133 // If u.Opaque is non-empty, String uses the first form;
134 // otherwise it uses the second form.
135 //
136 // In the second form, the following rules apply:
137 // - if u.Scheme is empty, scheme: is omitted.
138 // - if u.User is nil, userinfo@ is omitted.
139 // - if u.Host is empty, host/ is omitted.
140 // - if u.Scheme and u.Host are empty and u.User is nil,
141 // the entire scheme://userinfo@host/ is omitted.
142 // - if u.Host is non-empty and u.Path begins with a /,
143 // the form host/path does not add its own /.
144 // - if u.RawQuery is empty, ?query is omitted.
145 // - if u.Fragment is empty, #fragment is omitted.
146 func Escape(u *url.URL) string {
147 var buf bytes.Buffer
148 if u.Scheme != "" {
149 buf.WriteString(u.Scheme)
150 buf.WriteByte(':')
151 }
152 if u.Opaque != "" {
153 buf.WriteString(u.Opaque)
154 } else {
155 if u.Scheme != "" || u.Host != "" || u.User != nil {
156 buf.WriteString("//")
157 if ui := u.User; ui != nil {
158 buf.WriteString(unescapeUserinfo(ui.String()))
159 buf.WriteByte('@')
160 }
161 if h := u.Host; h != "" {
162 buf.WriteString(h)
163 }
164 }
165 if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
166 buf.WriteByte('/')
167 }
168 buf.WriteString(escape(u.Path, encodePath))
169 }
170 if u.RawQuery != "" {
171 buf.WriteByte('?')
172 buf.WriteString(u.RawQuery)
173 }
174 if u.Fragment != "" {
175 buf.WriteByte('#')
176 buf.WriteString(escape(u.Fragment, encodeFragment))
177 }
178 return buf.String()
179 }
0 // Copyright 2009 The Go Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package urlesc
5
6 import (
7 "net/url"
8 "testing"
9 )
10
11 type URLTest struct {
12 in string
13 out *url.URL
14 roundtrip string // expected result of reserializing the URL; empty means same as "in".
15 }
16
17 var urltests = []URLTest{
18 // no path
19 {
20 "http://www.google.com",
21 &url.URL{
22 Scheme: "http",
23 Host: "www.google.com",
24 },
25 "",
26 },
27 // path
28 {
29 "http://www.google.com/",
30 &url.URL{
31 Scheme: "http",
32 Host: "www.google.com",
33 Path: "/",
34 },
35 "",
36 },
37 // path with hex escaping
38 {
39 "http://www.google.com/file%20one%26two",
40 &url.URL{
41 Scheme: "http",
42 Host: "www.google.com",
43 Path: "/file one&two",
44 },
45 "http://www.google.com/file%20one&two",
46 },
47 // user
48 {
49 "ftp://webmaster@www.google.com/",
50 &url.URL{
51 Scheme: "ftp",
52 User: url.User("webmaster"),
53 Host: "www.google.com",
54 Path: "/",
55 },
56 "",
57 },
58 // escape sequence in username
59 {
60 "ftp://john%20doe@www.google.com/",
61 &url.URL{
62 Scheme: "ftp",
63 User: url.User("john doe"),
64 Host: "www.google.com",
65 Path: "/",
66 },
67 "ftp://john%20doe@www.google.com/",
68 },
69 // query
70 {
71 "http://www.google.com/?q=go+language",
72 &url.URL{
73 Scheme: "http",
74 Host: "www.google.com",
75 Path: "/",
76 RawQuery: "q=go+language",
77 },
78 "",
79 },
80 // query with hex escaping: NOT parsed
81 {
82 "http://www.google.com/?q=go%20language",
83 &url.URL{
84 Scheme: "http",
85 Host: "www.google.com",
86 Path: "/",
87 RawQuery: "q=go%20language",
88 },
89 "",
90 },
91 // %20 outside query
92 {
93 "http://www.google.com/a%20b?q=c+d",
94 &url.URL{
95 Scheme: "http",
96 Host: "www.google.com",
97 Path: "/a b",
98 RawQuery: "q=c+d",
99 },
100 "",
101 },
102 // path without leading /, so no parsing
103 {
104 "http:www.google.com/?q=go+language",
105 &url.URL{
106 Scheme: "http",
107 Opaque: "www.google.com/",
108 RawQuery: "q=go+language",
109 },
110 "http:www.google.com/?q=go+language",
111 },
112 // path without leading /, so no parsing
113 {
114 "http:%2f%2fwww.google.com/?q=go+language",
115 &url.URL{
116 Scheme: "http",
117 Opaque: "%2f%2fwww.google.com/",
118 RawQuery: "q=go+language",
119 },
120 "http:%2f%2fwww.google.com/?q=go+language",
121 },
122 // non-authority with path
123 {
124 "mailto:/webmaster@golang.org",
125 &url.URL{
126 Scheme: "mailto",
127 Path: "/webmaster@golang.org",
128 },
129 "mailto:///webmaster@golang.org", // unfortunate compromise
130 },
131 // non-authority
132 {
133 "mailto:webmaster@golang.org",
134 &url.URL{
135 Scheme: "mailto",
136 Opaque: "webmaster@golang.org",
137 },
138 "",
139 },
140 // unescaped :// in query should not create a scheme
141 {
142 "/foo?query=http://bad",
143 &url.URL{
144 Path: "/foo",
145 RawQuery: "query=http://bad",
146 },
147 "",
148 },
149 // leading // without scheme should create an authority
150 {
151 "//foo",
152 &url.URL{
153 Host: "foo",
154 },
155 "",
156 },
157 // leading // without scheme, with userinfo, path, and query
158 {
159 "//user@foo/path?a=b",
160 &url.URL{
161 User: url.User("user"),
162 Host: "foo",
163 Path: "/path",
164 RawQuery: "a=b",
165 },
166 "",
167 },
168 // Three leading slashes isn't an authority, but doesn't return an error.
169 // (We can't return an error, as this code is also used via
170 // ServeHTTP -> ReadRequest -> Parse, which is arguably a
171 // different URL parsing context, but currently shares the
172 // same codepath)
173 {
174 "///threeslashes",
175 &url.URL{
176 Path: "///threeslashes",
177 },
178 "",
179 },
180 {
181 "http://user:password@google.com",
182 &url.URL{
183 Scheme: "http",
184 User: url.UserPassword("user", "password"),
185 Host: "google.com",
186 },
187 "http://user:password@google.com",
188 },
189 // unescaped @ in username should not confuse host
190 {
191 "http://j@ne:password@google.com",
192 &url.URL{
193 Scheme: "http",
194 User: url.UserPassword("j@ne", "password"),
195 Host: "google.com",
196 },
197 "http://j%40ne:password@google.com",
198 },
199 // unescaped @ in password should not confuse host
200 {
201 "http://jane:p@ssword@google.com",
202 &url.URL{
203 Scheme: "http",
204 User: url.UserPassword("jane", "p@ssword"),
205 Host: "google.com",
206 },
207 "http://jane:p%40ssword@google.com",
208 },
209 {
210 "http://j@ne:password@google.com/p@th?q=@go",
211 &url.URL{
212 Scheme: "http",
213 User: url.UserPassword("j@ne", "password"),
214 Host: "google.com",
215 Path: "/p@th",
216 RawQuery: "q=@go",
217 },
218 "http://j%40ne:password@google.com/p@th?q=@go",
219 },
220 {
221 "http://www.google.com/?q=go+language#foo",
222 &url.URL{
223 Scheme: "http",
224 Host: "www.google.com",
225 Path: "/",
226 RawQuery: "q=go+language",
227 Fragment: "foo",
228 },
229 "",
230 },
231 {
232 "http://www.google.com/?q=go+language#foo%26bar",
233 &url.URL{
234 Scheme: "http",
235 Host: "www.google.com",
236 Path: "/",
237 RawQuery: "q=go+language",
238 Fragment: "foo&bar",
239 },
240 "http://www.google.com/?q=go+language#foo&bar",
241 },
242 {
243 "file:///home/adg/rabbits",
244 &url.URL{
245 Scheme: "file",
246 Host: "",
247 Path: "/home/adg/rabbits",
248 },
249 "file:///home/adg/rabbits",
250 },
251 // "Windows" paths are no exception to the rule.
252 // See golang.org/issue/6027, especially comment #9.
253 {
254 "file:///C:/FooBar/Baz.txt",
255 &url.URL{
256 Scheme: "file",
257 Host: "",
258 Path: "/C:/FooBar/Baz.txt",
259 },
260 "file:///C:/FooBar/Baz.txt",
261 },
262 // case-insensitive scheme
263 {
264 "MaIlTo:webmaster@golang.org",
265 &url.URL{
266 Scheme: "mailto",
267 Opaque: "webmaster@golang.org",
268 },
269 "mailto:webmaster@golang.org",
270 },
271 // Relative path
272 {
273 "a/b/c",
274 &url.URL{
275 Path: "a/b/c",
276 },
277 "a/b/c",
278 },
279 // escaped '?' in username and password
280 {
281 "http://%3Fam:pa%3Fsword@google.com",
282 &url.URL{
283 Scheme: "http",
284 User: url.UserPassword("?am", "pa?sword"),
285 Host: "google.com",
286 },
287 "",
288 },
289 // escaped '?' and '#' in path
290 {
291 "http://example.com/%3F%23",
292 &url.URL{
293 Scheme: "http",
294 Host: "example.com",
295 Path: "?#",
296 },
297 "",
298 },
299 // unescaped [ ] ! ' ( ) * in path
300 {
301 "http://example.com/[]!'()*",
302 &url.URL{
303 Scheme: "http",
304 Host: "example.com",
305 Path: "[]!'()*",
306 },
307 "http://example.com/[]!'()*",
308 },
309 // escaped : / ? # [ ] @ in username and password
310 {
311 "http://%3A%2F%3F:%23%5B%5D%40@example.com",
312 &url.URL{
313 Scheme: "http",
314 User: url.UserPassword(":/?", "#[]@"),
315 Host: "example.com",
316 },
317 "",
318 },
319 // unescaped ! $ & ' ( ) * + , ; = in username and password
320 {
321 "http://!$&'():*+,;=@example.com",
322 &url.URL{
323 Scheme: "http",
324 User: url.UserPassword("!$&'()", "*+,;="),
325 Host: "example.com",
326 },
327 "",
328 },
329 // unescaped = : / . ? = in query component
330 {
331 "http://example.com/?q=http://google.com/?q=",
332 &url.URL{
333 Scheme: "http",
334 Host: "example.com",
335 Path: "/",
336 RawQuery: "q=http://google.com/?q=",
337 },
338 "",
339 },
340 // unescaped : / ? # [ ] @ ! $ & ' ( ) * + , ; = in fragment
341 {
342 "http://example.com/#:/?#[]@!$&'()*+,;=",
343 &url.URL{
344 Scheme: "http",
345 Host: "example.com",
346 Path: "/",
347 Fragment: ":/?#[]@!$&'()*+,;=",
348 },
349 "",
350 },
351 }
352
353 func DoTestString(t *testing.T, parse func(string) (*url.URL, error), name string, tests []URLTest) {
354 for _, tt := range tests {
355 u, err := parse(tt.in)
356 if err != nil {
357 t.Errorf("%s(%q) returned error %s", name, tt.in, err)
358 continue
359 }
360 expected := tt.in
361 if len(tt.roundtrip) > 0 {
362 expected = tt.roundtrip
363 }
364 s := Escape(u)
365 if s != expected {
366 t.Errorf("Escape(%s(%q)) == %q (expected %q)", name, tt.in, s, expected)
367 }
368 }
369 }
370
371 func TestURLString(t *testing.T) {
372 DoTestString(t, url.Parse, "Parse", urltests)
373
374 // no leading slash on path should prepend
375 // slash on String() call
376 noslash := URLTest{
377 "http://www.google.com/search",
378 &url.URL{
379 Scheme: "http",
380 Host: "www.google.com",
381 Path: "search",
382 },
383 "",
384 }
385 s := Escape(noslash.out)
386 if s != noslash.in {
387 t.Errorf("Expected %s; go %s", noslash.in, s)
388 }
389 }
390
391 type EscapeTest struct {
392 in string
393 out string
394 err error
395 }
396
397 var escapeTests = []EscapeTest{
398 {
399 "",
400 "",
401 nil,
402 },
403 {
404 "abc",
405 "abc",
406 nil,
407 },
408 {
409 "one two",
410 "one+two",
411 nil,
412 },
413 {
414 "10%",
415 "10%25",
416 nil,
417 },
418 {
419 " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;",
420 "+?%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A/%40%24%27%28%29%2A%2C%3B",
421 nil,
422 },
423 }
424
425 func TestEscape(t *testing.T) {
426 for _, tt := range escapeTests {
427 actual := QueryEscape(tt.in)
428 if tt.out != actual {
429 t.Errorf("QueryEscape(%q) = %q, want %q", tt.in, actual, tt.out)
430 }
431
432 // for bonus points, verify that escape:unescape is an identity.
433 roundtrip, err := url.QueryUnescape(actual)
434 if roundtrip != tt.in || err != nil {
435 t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]")
436 }
437 }
438 }
439
440 var resolveReferenceTests = []struct {
441 base, rel, expected string
442 }{
443 // Absolute URL references
444 {"http://foo.com?a=b", "https://bar.com/", "https://bar.com/"},
445 {"http://foo.com/", "https://bar.com/?a=b", "https://bar.com/?a=b"},
446 {"http://foo.com/bar", "mailto:foo@example.com", "mailto:foo@example.com"},
447
448 // Path-absolute references
449 {"http://foo.com/bar", "/baz", "http://foo.com/baz"},
450 {"http://foo.com/bar?a=b#f", "/baz", "http://foo.com/baz"},
451 {"http://foo.com/bar?a=b", "/baz?c=d", "http://foo.com/baz?c=d"},
452
453 // Scheme-relative
454 {"https://foo.com/bar?a=b", "//bar.com/quux", "https://bar.com/quux"},
455
456 // Path-relative references:
457
458 // ... current directory
459 {"http://foo.com", ".", "http://foo.com/"},
460 {"http://foo.com/bar", ".", "http://foo.com/"},
461 {"http://foo.com/bar/", ".", "http://foo.com/bar/"},
462
463 // ... going down
464 {"http://foo.com", "bar", "http://foo.com/bar"},
465 {"http://foo.com/", "bar", "http://foo.com/bar"},
466 {"http://foo.com/bar/baz", "quux", "http://foo.com/bar/quux"},
467
468 // ... going up
469 {"http://foo.com/bar/baz", "../quux", "http://foo.com/quux"},
470 {"http://foo.com/bar/baz", "../../../../../quux", "http://foo.com/quux"},
471 {"http://foo.com/bar", "..", "http://foo.com/"},
472 {"http://foo.com/bar/baz", "./..", "http://foo.com/"},
473 // ".." in the middle (issue 3560)
474 {"http://foo.com/bar/baz", "quux/dotdot/../tail", "http://foo.com/bar/quux/tail"},
475 {"http://foo.com/bar/baz", "quux/./dotdot/../tail", "http://foo.com/bar/quux/tail"},
476 {"http://foo.com/bar/baz", "quux/./dotdot/.././tail", "http://foo.com/bar/quux/tail"},
477 {"http://foo.com/bar/baz", "quux/./dotdot/./../tail", "http://foo.com/bar/quux/tail"},
478 {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/././../../tail", "http://foo.com/bar/quux/tail"},
479 {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/./.././../tail", "http://foo.com/bar/quux/tail"},
480 {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/dotdot/./../../.././././tail", "http://foo.com/bar/quux/tail"},
481 {"http://foo.com/bar/baz", "quux/./dotdot/../dotdot/../dot/./tail/..", "http://foo.com/bar/quux/dot/"},
482
483 // Remove any dot-segments prior to forming the target URI.
484 // http://tools.ietf.org/html/rfc3986#section-5.2.4
485 {"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/baz"},
486
487 // Triple dot isn't special
488 {"http://foo.com/bar", "...", "http://foo.com/..."},
489
490 // Fragment
491 {"http://foo.com/bar", ".#frag", "http://foo.com/#frag"},
492
493 // RFC 3986: Normal Examples
494 // http://tools.ietf.org/html/rfc3986#section-5.4.1
495 {"http://a/b/c/d;p?q", "g:h", "g:h"},
496 {"http://a/b/c/d;p?q", "g", "http://a/b/c/g"},
497 {"http://a/b/c/d;p?q", "./g", "http://a/b/c/g"},
498 {"http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"},
499 {"http://a/b/c/d;p?q", "/g", "http://a/g"},
500 {"http://a/b/c/d;p?q", "//g", "http://g"},
501 {"http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"},
502 {"http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"},
503 {"http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"},
504 {"http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"},
505 {"http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"},
506 {"http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"},
507 {"http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"},
508 {"http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"},
509 {"http://a/b/c/d;p?q", "", "http://a/b/c/d;p?q"},
510 {"http://a/b/c/d;p?q", ".", "http://a/b/c/"},
511 {"http://a/b/c/d;p?q", "./", "http://a/b/c/"},
512 {"http://a/b/c/d;p?q", "..", "http://a/b/"},
513 {"http://a/b/c/d;p?q", "../", "http://a/b/"},
514 {"http://a/b/c/d;p?q", "../g", "http://a/b/g"},
515 {"http://a/b/c/d;p?q", "../..", "http://a/"},
516 {"http://a/b/c/d;p?q", "../../", "http://a/"},
517 {"http://a/b/c/d;p?q", "../../g", "http://a/g"},
518
519 // RFC 3986: Abnormal Examples
520 // http://tools.ietf.org/html/rfc3986#section-5.4.2
521 {"http://a/b/c/d;p?q", "../../../g", "http://a/g"},
522 {"http://a/b/c/d;p?q", "../../../../g", "http://a/g"},
523 {"http://a/b/c/d;p?q", "/./g", "http://a/g"},
524 {"http://a/b/c/d;p?q", "/../g", "http://a/g"},
525 {"http://a/b/c/d;p?q", "g.", "http://a/b/c/g."},
526 {"http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"},
527 {"http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."},
528 {"http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"},
529 {"http://a/b/c/d;p?q", "./../g", "http://a/b/g"},
530 {"http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"},
531 {"http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"},
532 {"http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"},
533 {"http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"},
534 {"http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"},
535 {"http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"},
536 {"http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"},
537 {"http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"},
538 {"http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"},
539
540 // Extras.
541 {"https://a/b/c/d;p?q", "//g?q", "https://g?q"},
542 {"https://a/b/c/d;p?q", "//g#s", "https://g#s"},
543 {"https://a/b/c/d;p?q", "//g/d/e/f?y#s", "https://g/d/e/f?y#s"},
544 {"https://a/b/c/d;p#s", "?y", "https://a/b/c/d;p?y"},
545 {"https://a/b/c/d;p?q#s", "?y", "https://a/b/c/d;p?y"},
546 }
547
548 func TestResolveReference(t *testing.T) {
549 mustParse := func(url_ string) *url.URL {
550 u, err := url.Parse(url_)
551 if err != nil {
552 t.Fatalf("Expected URL to parse: %q, got error: %v", url_, err)
553 }
554 return u
555 }
556 opaque := &url.URL{Scheme: "scheme", Opaque: "opaque"}
557 for _, test := range resolveReferenceTests {
558 base := mustParse(test.base)
559 rel := mustParse(test.rel)
560 url := base.ResolveReference(rel)
561 if Escape(url) != test.expected {
562 t.Errorf("URL(%q).ResolveReference(%q) == %q, got %q", test.base, test.rel, test.expected, Escape(url))
563 }
564 // Ensure that new instances are returned.
565 if base == url {
566 t.Errorf("Expected URL.ResolveReference to return new URL instance.")
567 }
568 // Test the convenience wrapper too.
569 url, err := base.Parse(test.rel)
570 if err != nil {
571 t.Errorf("URL(%q).Parse(%q) failed: %v", test.base, test.rel, err)
572 } else if Escape(url) != test.expected {
573 t.Errorf("URL(%q).Parse(%q) == %q, got %q", test.base, test.rel, test.expected, Escape(url))
574 } else if base == url {
575 // Ensure that new instances are returned for the wrapper too.
576 t.Errorf("Expected URL.Parse to return new URL instance.")
577 }
578 // Ensure Opaque resets the URL.
579 url = base.ResolveReference(opaque)
580 if *url != *opaque {
581 t.Errorf("ResolveReference failed to resolve opaque URL: want %#v, got %#v", url, opaque)
582 }
583 // Test the convenience wrapper with an opaque URL too.
584 url, err = base.Parse("scheme:opaque")
585 if err != nil {
586 t.Errorf(`URL(%q).Parse("scheme:opaque") failed: %v`, test.base, err)
587 } else if *url != *opaque {
588 t.Errorf("Parse failed to resolve opaque URL: want %#v, got %#v", url, opaque)
589 } else if base == url {
590 // Ensure that new instances are returned, again.
591 t.Errorf("Expected URL.Parse to return new URL instance.")
592 }
593 }
594 }
595
596 type shouldEscapeTest struct {
597 in byte
598 mode encoding
599 escape bool
600 }
601
602 var shouldEscapeTests = []shouldEscapeTest{
603 // Unreserved characters (§2.3)
604 {'a', encodePath, false},
605 {'a', encodeUserPassword, false},
606 {'a', encodeQueryComponent, false},
607 {'a', encodeFragment, false},
608 {'z', encodePath, false},
609 {'A', encodePath, false},
610 {'Z', encodePath, false},
611 {'0', encodePath, false},
612 {'9', encodePath, false},
613 {'-', encodePath, false},
614 {'-', encodeUserPassword, false},
615 {'-', encodeQueryComponent, false},
616 {'-', encodeFragment, false},
617 {'.', encodePath, false},
618 {'_', encodePath, false},
619 {'~', encodePath, false},
620
621 // User information (§3.2.1)
622 {':', encodeUserPassword, true},
623 {'/', encodeUserPassword, true},
624 {'?', encodeUserPassword, true},
625 {'@', encodeUserPassword, true},
626 {'$', encodeUserPassword, false},
627 {'&', encodeUserPassword, false},
628 {'+', encodeUserPassword, false},
629 {',', encodeUserPassword, false},
630 {';', encodeUserPassword, false},
631 {'=', encodeUserPassword, false},
632 }
633
634 func TestShouldEscape(t *testing.T) {
635 for _, tt := range shouldEscapeTests {
636 if shouldEscape(tt.in, tt.mode) != tt.escape {
637 t.Errorf("shouldEscape(%q, %v) returned %v; expected %v", tt.in, tt.mode, !tt.escape, tt.escape)
638 }
639 }
640 }