Skip to content

Commit 77be396

Browse files
authored
add support for PathValue and SetPathValue from go v1.23.7 (#38)
* add support for PathValue and SetPathValue from go v1.23.7 * add TINYGO notice to pattern.go, proper copy paste of PathValue func
1 parent ca7cd08 commit 77be396

File tree

3 files changed

+367
-1
lines changed

3 files changed

+367
-1
lines changed

http/pattern.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// TINYGO: The following is copied and modified from Go 1.23.7 official implementation.
2+
3+
// Copyright 2023 The Go Authors. All rights reserved.
4+
// Use of this source code is governed by a BSD-style
5+
// license that can be found in the LICENSE file.
6+
7+
// Patterns for ServeMux routing.
8+
9+
package http
10+
11+
import (
12+
"errors"
13+
"fmt"
14+
"net/url"
15+
"strings"
16+
"unicode"
17+
)
18+
19+
// A pattern is something that can be matched against an HTTP request.
20+
// It has an optional method, an optional host, and a path.
21+
type pattern struct {
22+
str string // original string
23+
method string
24+
host string
25+
// The representation of a path differs from the surface syntax, which
26+
// simplifies most algorithms.
27+
//
28+
// Paths ending in '/' are represented with an anonymous "..." wildcard.
29+
// For example, the path "a/" is represented as a literal segment "a" followed
30+
// by a segment with multi==true.
31+
//
32+
// Paths ending in "{$}" are represented with the literal segment "/".
33+
// For example, the path "a/{$}" is represented as a literal segment "a" followed
34+
// by a literal segment "/".
35+
segments []segment
36+
loc string // source location of registering call, for helpful messages
37+
}
38+
39+
func (p *pattern) String() string { return p.str }
40+
41+
func (p *pattern) lastSegment() segment {
42+
return p.segments[len(p.segments)-1]
43+
}
44+
45+
// A segment is a pattern piece that matches one or more path segments, or
46+
// a trailing slash.
47+
//
48+
// If wild is false, it matches a literal segment, or, if s == "/", a trailing slash.
49+
// Examples:
50+
//
51+
// "a" => segment{s: "a"}
52+
// "/{$}" => segment{s: "/"}
53+
//
54+
// If wild is true and multi is false, it matches a single path segment.
55+
// Example:
56+
//
57+
// "{x}" => segment{s: "x", wild: true}
58+
//
59+
// If both wild and multi are true, it matches all remaining path segments.
60+
// Example:
61+
//
62+
// "{rest...}" => segment{s: "rest", wild: true, multi: true}
63+
type segment struct {
64+
s string // literal or wildcard name or "/" for "/{$}".
65+
wild bool
66+
multi bool // "..." wildcard
67+
}
68+
69+
// parsePattern parses a string into a Pattern.
70+
// The string's syntax is
71+
//
72+
// [METHOD] [HOST]/[PATH]
73+
//
74+
// where:
75+
// - METHOD is an HTTP method
76+
// - HOST is a hostname
77+
// - PATH consists of slash-separated segments, where each segment is either
78+
// a literal or a wildcard of the form "{name}", "{name...}", or "{$}".
79+
//
80+
// METHOD, HOST and PATH are all optional; that is, the string can be "/".
81+
// If METHOD is present, it must be followed by at least one space or tab.
82+
// Wildcard names must be valid Go identifiers.
83+
// The "{$}" and "{name...}" wildcard must occur at the end of PATH.
84+
// PATH may end with a '/'.
85+
// Wildcard names in a path must be distinct.
86+
func parsePattern(s string) (_ *pattern, err error) {
87+
if len(s) == 0 {
88+
return nil, errors.New("empty pattern")
89+
}
90+
off := 0 // offset into string
91+
defer func() {
92+
if err != nil {
93+
err = fmt.Errorf("at offset %d: %w", off, err)
94+
}
95+
}()
96+
97+
method, rest, found := s, "", false
98+
if i := strings.IndexAny(s, " \t"); i >= 0 {
99+
method, rest, found = s[:i], strings.TrimLeft(s[i+1:], " \t"), true
100+
}
101+
if !found {
102+
rest = method
103+
method = ""
104+
}
105+
if method != "" && !validMethod(method) {
106+
return nil, fmt.Errorf("invalid method %q", method)
107+
}
108+
p := &pattern{str: s, method: method}
109+
110+
if found {
111+
off = len(method) + 1
112+
}
113+
i := strings.IndexByte(rest, '/')
114+
if i < 0 {
115+
return nil, errors.New("host/path missing /")
116+
}
117+
p.host = rest[:i]
118+
rest = rest[i:]
119+
if j := strings.IndexByte(p.host, '{'); j >= 0 {
120+
off += j
121+
return nil, errors.New("host contains '{' (missing initial '/'?)")
122+
}
123+
// At this point, rest is the path.
124+
off += i
125+
126+
// An unclean path with a method that is not CONNECT can never match,
127+
// because paths are cleaned before matching.
128+
if method != "" && method != "CONNECT" && rest != cleanPath(rest) {
129+
return nil, errors.New("non-CONNECT pattern with unclean path can never match")
130+
}
131+
132+
seenNames := map[string]bool{} // remember wildcard names to catch dups
133+
for len(rest) > 0 {
134+
// Invariant: rest[0] == '/'.
135+
rest = rest[1:]
136+
off = len(s) - len(rest)
137+
if len(rest) == 0 {
138+
// Trailing slash.
139+
p.segments = append(p.segments, segment{wild: true, multi: true})
140+
break
141+
}
142+
i := strings.IndexByte(rest, '/')
143+
if i < 0 {
144+
i = len(rest)
145+
}
146+
var seg string
147+
seg, rest = rest[:i], rest[i:]
148+
if i := strings.IndexByte(seg, '{'); i < 0 {
149+
// Literal.
150+
seg = pathUnescape(seg)
151+
p.segments = append(p.segments, segment{s: seg})
152+
} else {
153+
// Wildcard.
154+
if i != 0 {
155+
return nil, errors.New("bad wildcard segment (must start with '{')")
156+
}
157+
if seg[len(seg)-1] != '}' {
158+
return nil, errors.New("bad wildcard segment (must end with '}')")
159+
}
160+
name := seg[1 : len(seg)-1]
161+
if name == "$" {
162+
if len(rest) != 0 {
163+
return nil, errors.New("{$} not at end")
164+
}
165+
p.segments = append(p.segments, segment{s: "/"})
166+
break
167+
}
168+
name, multi := strings.CutSuffix(name, "...")
169+
if multi && len(rest) != 0 {
170+
return nil, errors.New("{...} wildcard not at end")
171+
}
172+
if name == "" {
173+
return nil, errors.New("empty wildcard")
174+
}
175+
if !isValidWildcardName(name) {
176+
return nil, fmt.Errorf("bad wildcard name %q", name)
177+
}
178+
if seenNames[name] {
179+
return nil, fmt.Errorf("duplicate wildcard name %q", name)
180+
}
181+
seenNames[name] = true
182+
p.segments = append(p.segments, segment{s: name, wild: true, multi: multi})
183+
}
184+
}
185+
return p, nil
186+
}
187+
188+
func isValidWildcardName(s string) bool {
189+
if s == "" {
190+
return false
191+
}
192+
// Valid Go identifier.
193+
for i, c := range s {
194+
if !unicode.IsLetter(c) && c != '_' && (i == 0 || !unicode.IsDigit(c)) {
195+
return false
196+
}
197+
}
198+
return true
199+
}
200+
201+
func pathUnescape(path string) string {
202+
u, err := url.PathUnescape(path)
203+
if err != nil {
204+
// Invalidly escaped path; use the original
205+
return path
206+
}
207+
return u
208+
}

http/request.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ type Request struct {
336336
// TINYGO: Add onEOF func for callback when response is fully read
337337
// TINYGO: so we can close the connection.
338338
onEOF func()
339+
340+
// The following fields are for requests matched by ServeMux.
341+
pat *pattern // the pattern that matched
342+
matches []string // values for the matching wildcards in pat
343+
otherValues map[string]string // for calls to SetPathValue that don't match a wildcard
339344
}
340345

341346
// Context returns the request's context. To change the context, use
@@ -403,6 +408,20 @@ func (r *Request) Clone(ctx context.Context) *Request {
403408
r2.Form = cloneURLValues(r.Form)
404409
r2.PostForm = cloneURLValues(r.PostForm)
405410
r2.MultipartForm = cloneMultipartForm(r.MultipartForm)
411+
412+
// Copy matches and otherValues. See issue 61410.
413+
if s := r.matches; s != nil {
414+
s2 := make([]string, len(s))
415+
copy(s2, s)
416+
r2.matches = s2
417+
}
418+
if s := r.otherValues; s != nil {
419+
s2 := make(map[string]string, len(s))
420+
for k, v := range s {
421+
s2[k] = v
422+
}
423+
r2.otherValues = s2
424+
}
406425
return r2
407426
}
408427

@@ -1456,3 +1475,47 @@ func (r *Request) requiresHTTP1() bool {
14561475
return hasToken(r.Header.Get("Connection"), "upgrade") &&
14571476
ascii.EqualFold(r.Header.Get("Upgrade"), "websocket")
14581477
}
1478+
1479+
// PathValue returns the value for the named path wildcard in the [ServeMux] pattern
1480+
// that matched the request.
1481+
// It returns the empty string if the request was not matched against a pattern
1482+
// or there is no such wildcard in the pattern.
1483+
func (r *Request) PathValue(name string) string {
1484+
if i := r.patIndex(name); i >= 0 {
1485+
return r.matches[i]
1486+
}
1487+
return r.otherValues[name]
1488+
}
1489+
1490+
// SetPathValue sets name to value, so that subsequent calls to r.PathValue(name)
1491+
// return value.
1492+
func (r *Request) SetPathValue(name, value string) {
1493+
if i := r.patIndex(name); i >= 0 {
1494+
r.matches[i] = value
1495+
} else {
1496+
if r.otherValues == nil {
1497+
r.otherValues = map[string]string{}
1498+
}
1499+
r.otherValues[name] = value
1500+
}
1501+
}
1502+
1503+
// patIndex returns the index of name in the list of named wildcards of the
1504+
// request's pattern, or -1 if there is no such name.
1505+
func (r *Request) patIndex(name string) int {
1506+
// The linear search seems expensive compared to a map, but just creating the map
1507+
// takes a lot of time, and most patterns will just have a couple of wildcards.
1508+
if r.pat == nil {
1509+
return -1
1510+
}
1511+
i := 0
1512+
for _, seg := range r.pat.segments {
1513+
if seg.wild && seg.s != "" {
1514+
if name == seg.s {
1515+
return i
1516+
}
1517+
i++
1518+
}
1519+
}
1520+
return -1
1521+
}

0 commit comments

Comments
 (0)