Skip to content

Commit 7e2bf95

Browse files
rscbradfitz
authored andcommitted
net/url: add PathEscape, PathUnescape
Fixes #13737. Change-Id: Ib655dbf06f44709f687f8a2410c80f31e4075f13 Reviewed-on: https://go-review.googlesource.com/31322 Run-TryBot: Brad Fitzpatrick <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 59dae58 commit 7e2bf95

File tree

2 files changed

+106
-4
lines changed

2 files changed

+106
-4
lines changed

src/net/url/url.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ type encoding int
7474

7575
const (
7676
encodePath encoding = 1 + iota
77+
encodePathSegment
7778
encodeHost
7879
encodeZone
7980
encodeUserPassword
@@ -132,9 +133,14 @@ func shouldEscape(c byte, mode encoding) bool {
132133
// The RFC allows : @ & = + $ but saves / ; , for assigning
133134
// meaning to individual path segments. This package
134135
// only manipulates the path as a whole, so we allow those
135-
// last two as well. That leaves only ? to escape.
136+
// last three as well. That leaves only ? to escape.
136137
return c == '?'
137138

139+
case encodePathSegment: // §3.3
140+
// The RFC allows : @ & = + $ but saves / ; , for assigning
141+
// meaning to individual path segments.
142+
return c == '/' || c == ';' || c == ',' || c == '?'
143+
138144
case encodeUserPassword: // §3.2.1
139145
// The RFC allows ';', ':', '&', '=', '+', '$', and ',' in
140146
// userinfo, so we must escape only '@', '/', and '?'.
@@ -164,6 +170,15 @@ func QueryUnescape(s string) (string, error) {
164170
return unescape(s, encodeQueryComponent)
165171
}
166172

173+
// PathUnescape does the inverse transformation of PathEscape, converting
174+
// %AB into the byte 0xAB. It returns an error if any % is not followed by
175+
// two hexadecimal digits.
176+
//
177+
// PathUnescape is identical to QueryUnescape except that it does not unescape '+' to ' ' (space).
178+
func PathUnescape(s string) (string, error) {
179+
return unescape(s, encodePathSegment)
180+
}
181+
167182
// unescape unescapes a string; the mode specifies
168183
// which section of the URL string is being unescaped.
169184
func unescape(s string, mode encoding) (string, error) {
@@ -250,6 +265,12 @@ func QueryEscape(s string) string {
250265
return escape(s, encodeQueryComponent)
251266
}
252267

268+
// PathEscape escapes the string so it can be safely placed
269+
// inside a URL path segment.
270+
func PathEscape(s string) string {
271+
return escape(s, encodePathSegment)
272+
}
273+
253274
func escape(s string, mode encoding) string {
254275
spaceCount, hexCount := 0, 0
255276
for i := 0; i < len(s); i++ {

src/net/url/url_test.go

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,16 @@ var unescapeTests = []EscapeTest{
800800
"",
801801
EscapeError("%zz"),
802802
},
803+
{
804+
"a+b",
805+
"a b",
806+
nil,
807+
},
808+
{
809+
"a%20b",
810+
"a b",
811+
nil,
812+
},
803813
}
804814

805815
func TestUnescape(t *testing.T) {
@@ -808,10 +818,33 @@ func TestUnescape(t *testing.T) {
808818
if actual != tt.out || (err != nil) != (tt.err != nil) {
809819
t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", tt.in, actual, err, tt.out, tt.err)
810820
}
821+
822+
in := tt.in
823+
out := tt.out
824+
if strings.Contains(tt.in, "+") {
825+
in = strings.Replace(tt.in, "+", "%20", -1)
826+
actual, err := PathUnescape(in)
827+
if actual != tt.out || (err != nil) != (tt.err != nil) {
828+
t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, tt.out, tt.err)
829+
}
830+
if tt.err == nil {
831+
s, err := QueryUnescape(strings.Replace(tt.in, "+", "XXX", -1))
832+
if err != nil {
833+
continue
834+
}
835+
in = tt.in
836+
out = strings.Replace(s, "XXX", "+", -1)
837+
}
838+
}
839+
840+
actual, err = PathUnescape(in)
841+
if actual != out || (err != nil) != (tt.err != nil) {
842+
t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, out, tt.err)
843+
}
811844
}
812845
}
813846

814-
var escapeTests = []EscapeTest{
847+
var queryEscapeTests = []EscapeTest{
815848
{
816849
"",
817850
"",
@@ -839,8 +872,8 @@ var escapeTests = []EscapeTest{
839872
},
840873
}
841874

842-
func TestEscape(t *testing.T) {
843-
for _, tt := range escapeTests {
875+
func TestQueryEscape(t *testing.T) {
876+
for _, tt := range queryEscapeTests {
844877
actual := QueryEscape(tt.in)
845878
if tt.out != actual {
846879
t.Errorf("QueryEscape(%q) = %q, want %q", tt.in, actual, tt.out)
@@ -854,6 +887,54 @@ func TestEscape(t *testing.T) {
854887
}
855888
}
856889

890+
var pathEscapeTests = []EscapeTest{
891+
{
892+
"",
893+
"",
894+
nil,
895+
},
896+
{
897+
"abc",
898+
"abc",
899+
nil,
900+
},
901+
{
902+
"abc+def",
903+
"abc+def",
904+
nil,
905+
},
906+
{
907+
"one two",
908+
"one%20two",
909+
nil,
910+
},
911+
{
912+
"10%",
913+
"10%25",
914+
nil,
915+
},
916+
{
917+
" ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;",
918+
"%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B",
919+
nil,
920+
},
921+
}
922+
923+
func TestPathEscape(t *testing.T) {
924+
for _, tt := range pathEscapeTests {
925+
actual := PathEscape(tt.in)
926+
if tt.out != actual {
927+
t.Errorf("PathEscape(%q) = %q, want %q", tt.in, actual, tt.out)
928+
}
929+
930+
// for bonus points, verify that escape:unescape is an identity.
931+
roundtrip, err := PathUnescape(actual)
932+
if roundtrip != tt.in || err != nil {
933+
t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]")
934+
}
935+
}
936+
}
937+
857938
//var userinfoTests = []UserinfoTest{
858939
// {"user", "password", "user:password"},
859940
// {"foo:bar", "~!@#$%^&*()_+{}|[]\\-=`:;'\"<>?,./",

0 commit comments

Comments
 (0)