Skip to content

Commit 8c00e07

Browse files
committed
net/url: add URL.RawFragment, URL.EscapedFragment
These are analogous to URL.RawPath and URL.EscapedPath and allow users fine-grained control over how the fragment section of the URL is escaped. Some tools care about / vs %2f, same problem as in paths. Fixes #37776. Change-Id: Ie6f556d86bdff750c47fe65398cbafd834152b47 Reviewed-on: https://go-review.googlesource.com/c/go/+/227645 Reviewed-by: Emmanuel Odeke <emm.odeke@gmail.com>
1 parent d4d2980 commit 8c00e07

File tree

4 files changed

+112
-26
lines changed

4 files changed

+112
-26
lines changed

doc/go1.15.html

+10-3
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ <h3 id="minor_library_changes">Minor changes to the library</h3>
138138
<p><!-- CL 221427 -->
139139
When the flag package sees <code>-h</code> or <code>-help</code>, and
140140
those flags are not defined, the flag package prints a usage message.
141-
If the <a href=/pkg/flag/#FlagSet><code>FlagSet</code></a> was created with
142-
<a href=/pkg/flag/#ExitOnError><code>ExitOnError</code></a>,
143-
<a href=/pkg/flag/#FlagSet.Parse><code>FlagSet.Parse</code></a> would then
141+
If the <a href="/pkg/flag/#FlagSet"><code>FlagSet</code></a> was created with
142+
<a href="/pkg/flag/#ExitOnError"><code>ExitOnError</code></a>,
143+
<a href="/pkg/flag/#FlagSet.Parse"><code>FlagSet.Parse</code></a> would then
144144
exit with a status of 2. In this release, the exit status for <code>-h</code>
145145
or <code>-help</code> has been changed to 0. In particular, this applies to
146146
the default handling of command line flags.
@@ -150,6 +150,13 @@ <h3 id="minor_library_changes">Minor changes to the library</h3>
150150

151151
<dl id="net/url"><dt><a href="/pkg/net/url/">net/url</a></dt>
152152
<dd>
153+
<p><!-- CL 227645 -->
154+
The new <a href="/pkg/net/url/#URL"><code>URL</code></a> field
155+
<code>RawFragment</code> and method <a href="/pkg/net/url/#URL.EscapedFragment"><code>EscapedFragment</code></a>
156+
provide detail about and control over the exact encoding of a particular fragment.
157+
These are analogous to
158+
<code>RawPath</code> and <a href="/pkg/net/url/#URL.EscapedPath"><code>EscapedPath</code></a>.
159+
</p>
153160
<p><!-- CL 207082 -->
154161
The new <a href="/pkg/net/url/#URL"><code>URL</code></a>
155162
method <a href="/pkg/net/url/#URL.Redacted"><code>Redacted</code></a>

src/net/url/example_test.go

+21-3
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,31 @@ func ExampleParseQuery() {
8282
}
8383

8484
func ExampleURL_EscapedPath() {
85-
u, err := url.Parse("http://example.com/path with spaces")
85+
u, err := url.Parse("http://example.com/x/y%2Fz")
8686
if err != nil {
8787
log.Fatal(err)
8888
}
89-
fmt.Println(u.EscapedPath())
89+
fmt.Println("Path:", u.Path)
90+
fmt.Println("RawPath:", u.RawPath)
91+
fmt.Println("EscapedPath:", u.EscapedPath())
9092
// Output:
91-
// /path%20with%20spaces
93+
// Path: /x/y/z
94+
// RawPath: /x/y%2Fz
95+
// EscapedPath: /x/y%2Fz
96+
}
97+
98+
func ExampleURL_EscapedFragment() {
99+
u, err := url.Parse("http://example.com/#x/y%2Fz")
100+
if err != nil {
101+
log.Fatal(err)
102+
}
103+
fmt.Println("Fragment:", u.Fragment)
104+
fmt.Println("RawFragment:", u.RawFragment)
105+
fmt.Println("EscapedFragment:", u.EscapedFragment())
106+
// Output:
107+
// Fragment: x/y/z
108+
// RawFragment: x/y%2Fz
109+
// EscapedFragment: x/y%2Fz
92110
}
93111

94112
func ExampleURL_Hostname() {

src/net/url/url.go

+53-16
Original file line numberDiff line numberDiff line change
@@ -356,15 +356,16 @@ func escape(s string, mode encoding) string {
356356
// URL's String method uses the EscapedPath method to obtain the path. See the
357357
// EscapedPath method for more details.
358358
type URL struct {
359-
Scheme string
360-
Opaque string // encoded opaque data
361-
User *Userinfo // username and password information
362-
Host string // host or host:port
363-
Path string // path (relative paths may omit leading slash)
364-
RawPath string // encoded path hint (see EscapedPath method)
365-
ForceQuery bool // append a query ('?') even if RawQuery is empty
366-
RawQuery string // encoded query values, without '?'
367-
Fragment string // fragment for references, without '#'
359+
Scheme string
360+
Opaque string // encoded opaque data
361+
User *Userinfo // username and password information
362+
Host string // host or host:port
363+
Path string // path (relative paths may omit leading slash)
364+
RawPath string // encoded path hint (see EscapedPath method)
365+
ForceQuery bool // append a query ('?') even if RawQuery is empty
366+
RawQuery string // encoded query values, without '?'
367+
Fragment string // fragment for references, without '#'
368+
RawFragment string // encoded fragment hint (see EscapedFragment method)
368369
}
369370

370371
// User returns a Userinfo containing the provided username
@@ -481,7 +482,7 @@ func Parse(rawurl string) (*URL, error) {
481482
if frag == "" {
482483
return url, nil
483484
}
484-
if url.Fragment, err = unescape(frag, encodeFragment); err != nil {
485+
if err = url.setFragment(frag); err != nil {
485486
return nil, &Error{"parse", rawurl, err}
486487
}
487488
return url, nil
@@ -697,7 +698,7 @@ func (u *URL) setPath(p string) error {
697698
// In general, code should call EscapedPath instead of
698699
// reading u.RawPath directly.
699700
func (u *URL) EscapedPath() string {
700-
if u.RawPath != "" && validEncodedPath(u.RawPath) {
701+
if u.RawPath != "" && validEncoded(u.RawPath, encodePath) {
701702
p, err := unescape(u.RawPath, encodePath)
702703
if err == nil && p == u.Path {
703704
return u.RawPath
@@ -709,9 +710,10 @@ func (u *URL) EscapedPath() string {
709710
return escape(u.Path, encodePath)
710711
}
711712

712-
// validEncodedPath reports whether s is a valid encoded path.
713-
// It must not contain any bytes that require escaping during path encoding.
714-
func validEncodedPath(s string) bool {
713+
// validEncoded reports whether s is a valid encoded path or fragment,
714+
// according to mode.
715+
// It must not contain any bytes that require escaping during encoding.
716+
func validEncoded(s string, mode encoding) bool {
715717
for i := 0; i < len(s); i++ {
716718
// RFC 3986, Appendix A.
717719
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@".
@@ -726,14 +728,48 @@ func validEncodedPath(s string) bool {
726728
case '%':
727729
// ok - percent encoded, will decode
728730
default:
729-
if shouldEscape(s[i], encodePath) {
731+
if shouldEscape(s[i], mode) {
730732
return false
731733
}
732734
}
733735
}
734736
return true
735737
}
736738

739+
// setFragment is like setPath but for Fragment/RawFragment.
740+
func (u *URL) setFragment(f string) error {
741+
frag, err := unescape(f, encodeFragment)
742+
if err != nil {
743+
return err
744+
}
745+
u.Fragment = frag
746+
if escf := escape(frag, encodeFragment); f == escf {
747+
// Default encoding is fine.
748+
u.RawFragment = ""
749+
} else {
750+
u.RawFragment = f
751+
}
752+
return nil
753+
}
754+
755+
// EscapedFragment returns the escaped form of u.Fragment.
756+
// In general there are multiple possible escaped forms of any fragment.
757+
// EscapedFragment returns u.RawFragment when it is a valid escaping of u.Fragment.
758+
// Otherwise EscapedFragment ignores u.RawFragment and computes an escaped
759+
// form on its own.
760+
// The String method uses EscapedFragment to construct its result.
761+
// In general, code should call EscapedFragment instead of
762+
// reading u.RawFragment directly.
763+
func (u *URL) EscapedFragment() string {
764+
if u.RawFragment != "" && validEncoded(u.RawFragment, encodeFragment) {
765+
f, err := unescape(u.RawFragment, encodeFragment)
766+
if err == nil && f == u.Fragment {
767+
return u.RawFragment
768+
}
769+
}
770+
return escape(u.Fragment, encodeFragment)
771+
}
772+
737773
// validOptionalPort reports whether port is either an empty string
738774
// or matches /^:\d*$/
739775
func validOptionalPort(port string) bool {
@@ -816,7 +852,7 @@ func (u *URL) String() string {
816852
}
817853
if u.Fragment != "" {
818854
buf.WriteByte('#')
819-
buf.WriteString(escape(u.Fragment, encodeFragment))
855+
buf.WriteString(u.EscapedFragment())
820856
}
821857
return buf.String()
822858
}
@@ -1030,6 +1066,7 @@ func (u *URL) ResolveReference(ref *URL) *URL {
10301066
url.RawQuery = u.RawQuery
10311067
if ref.Fragment == "" {
10321068
url.Fragment = u.Fragment
1069+
url.RawFragment = u.RawFragment
10331070
}
10341071
}
10351072
// The "abs_path" or "rel_path" cases.

src/net/url/url_test.go

+28-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919

2020
type URLTest struct {
2121
in string
22-
out *URL // expected parse; RawPath="" means same as Path
22+
out *URL // expected parse
2323
roundtrip string // expected result of reserializing the URL; empty means same as "in".
2424
}
2525

@@ -54,6 +54,18 @@ var urltests = []URLTest{
5454
},
5555
"",
5656
},
57+
// fragment with hex escaping
58+
{
59+
"http://www.google.com/#file%20one%26two",
60+
&URL{
61+
Scheme: "http",
62+
Host: "www.google.com",
63+
Path: "/",
64+
Fragment: "file one&two",
65+
RawFragment: "file%20one%26two",
66+
},
67+
"",
68+
},
5769
// user
5870
{
5971
"ftp://webmaster@www.google.com/",
@@ -261,7 +273,7 @@ var urltests = []URLTest{
261273
"",
262274
},
263275
{
264-
"http://www.google.com/?q=go+language#foo%26bar",
276+
"http://www.google.com/?q=go+language#foo&bar",
265277
&URL{
266278
Scheme: "http",
267279
Host: "www.google.com",
@@ -271,6 +283,18 @@ var urltests = []URLTest{
271283
},
272284
"http://www.google.com/?q=go+language#foo&bar",
273285
},
286+
{
287+
"http://www.google.com/?q=go+language#foo%26bar",
288+
&URL{
289+
Scheme: "http",
290+
Host: "www.google.com",
291+
Path: "/",
292+
RawQuery: "q=go+language",
293+
Fragment: "foo&bar",
294+
RawFragment: "foo%26bar",
295+
},
296+
"http://www.google.com/?q=go+language#foo%26bar",
297+
},
274298
{
275299
"file:///home/adg/rabbits",
276300
&URL{
@@ -601,8 +625,8 @@ func ufmt(u *URL) string {
601625
pass = p
602626
}
603627
}
604-
return fmt.Sprintf("opaque=%q, scheme=%q, user=%#v, pass=%#v, host=%q, path=%q, rawpath=%q, rawq=%q, frag=%q, forcequery=%v",
605-
u.Opaque, u.Scheme, user, pass, u.Host, u.Path, u.RawPath, u.RawQuery, u.Fragment, u.ForceQuery)
628+
return fmt.Sprintf("opaque=%q, scheme=%q, user=%#v, pass=%#v, host=%q, path=%q, rawpath=%q, rawq=%q, frag=%q, rawfrag=%q, forcequery=%v",
629+
u.Opaque, u.Scheme, user, pass, u.Host, u.Path, u.RawPath, u.RawQuery, u.Fragment, u.RawFragment, u.ForceQuery)
606630
}
607631

608632
func BenchmarkString(b *testing.B) {

0 commit comments

Comments
 (0)