mirror of
https://github.com/tinygo-org/net.git
synced 2026-08-19 03:53:56 +00:00
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
This commit is contained in:
+208
@@ -0,0 +1,208 @@
|
|||||||
|
// TINYGO: The following is copied and modified from Go 1.23.7 official implementation.
|
||||||
|
|
||||||
|
// Copyright 2023 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Patterns for ServeMux routing.
|
||||||
|
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A pattern is something that can be matched against an HTTP request.
|
||||||
|
// It has an optional method, an optional host, and a path.
|
||||||
|
type pattern struct {
|
||||||
|
str string // original string
|
||||||
|
method string
|
||||||
|
host string
|
||||||
|
// The representation of a path differs from the surface syntax, which
|
||||||
|
// simplifies most algorithms.
|
||||||
|
//
|
||||||
|
// Paths ending in '/' are represented with an anonymous "..." wildcard.
|
||||||
|
// For example, the path "a/" is represented as a literal segment "a" followed
|
||||||
|
// by a segment with multi==true.
|
||||||
|
//
|
||||||
|
// Paths ending in "{$}" are represented with the literal segment "/".
|
||||||
|
// For example, the path "a/{$}" is represented as a literal segment "a" followed
|
||||||
|
// by a literal segment "/".
|
||||||
|
segments []segment
|
||||||
|
loc string // source location of registering call, for helpful messages
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pattern) String() string { return p.str }
|
||||||
|
|
||||||
|
func (p *pattern) lastSegment() segment {
|
||||||
|
return p.segments[len(p.segments)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// A segment is a pattern piece that matches one or more path segments, or
|
||||||
|
// a trailing slash.
|
||||||
|
//
|
||||||
|
// If wild is false, it matches a literal segment, or, if s == "/", a trailing slash.
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// "a" => segment{s: "a"}
|
||||||
|
// "/{$}" => segment{s: "/"}
|
||||||
|
//
|
||||||
|
// If wild is true and multi is false, it matches a single path segment.
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "{x}" => segment{s: "x", wild: true}
|
||||||
|
//
|
||||||
|
// If both wild and multi are true, it matches all remaining path segments.
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// "{rest...}" => segment{s: "rest", wild: true, multi: true}
|
||||||
|
type segment struct {
|
||||||
|
s string // literal or wildcard name or "/" for "/{$}".
|
||||||
|
wild bool
|
||||||
|
multi bool // "..." wildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePattern parses a string into a Pattern.
|
||||||
|
// The string's syntax is
|
||||||
|
//
|
||||||
|
// [METHOD] [HOST]/[PATH]
|
||||||
|
//
|
||||||
|
// where:
|
||||||
|
// - METHOD is an HTTP method
|
||||||
|
// - HOST is a hostname
|
||||||
|
// - PATH consists of slash-separated segments, where each segment is either
|
||||||
|
// a literal or a wildcard of the form "{name}", "{name...}", or "{$}".
|
||||||
|
//
|
||||||
|
// METHOD, HOST and PATH are all optional; that is, the string can be "/".
|
||||||
|
// If METHOD is present, it must be followed by at least one space or tab.
|
||||||
|
// Wildcard names must be valid Go identifiers.
|
||||||
|
// The "{$}" and "{name...}" wildcard must occur at the end of PATH.
|
||||||
|
// PATH may end with a '/'.
|
||||||
|
// Wildcard names in a path must be distinct.
|
||||||
|
func parsePattern(s string) (_ *pattern, err error) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return nil, errors.New("empty pattern")
|
||||||
|
}
|
||||||
|
off := 0 // offset into string
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("at offset %d: %w", off, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
method, rest, found := s, "", false
|
||||||
|
if i := strings.IndexAny(s, " \t"); i >= 0 {
|
||||||
|
method, rest, found = s[:i], strings.TrimLeft(s[i+1:], " \t"), true
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
rest = method
|
||||||
|
method = ""
|
||||||
|
}
|
||||||
|
if method != "" && !validMethod(method) {
|
||||||
|
return nil, fmt.Errorf("invalid method %q", method)
|
||||||
|
}
|
||||||
|
p := &pattern{str: s, method: method}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
off = len(method) + 1
|
||||||
|
}
|
||||||
|
i := strings.IndexByte(rest, '/')
|
||||||
|
if i < 0 {
|
||||||
|
return nil, errors.New("host/path missing /")
|
||||||
|
}
|
||||||
|
p.host = rest[:i]
|
||||||
|
rest = rest[i:]
|
||||||
|
if j := strings.IndexByte(p.host, '{'); j >= 0 {
|
||||||
|
off += j
|
||||||
|
return nil, errors.New("host contains '{' (missing initial '/'?)")
|
||||||
|
}
|
||||||
|
// At this point, rest is the path.
|
||||||
|
off += i
|
||||||
|
|
||||||
|
// An unclean path with a method that is not CONNECT can never match,
|
||||||
|
// because paths are cleaned before matching.
|
||||||
|
if method != "" && method != "CONNECT" && rest != cleanPath(rest) {
|
||||||
|
return nil, errors.New("non-CONNECT pattern with unclean path can never match")
|
||||||
|
}
|
||||||
|
|
||||||
|
seenNames := map[string]bool{} // remember wildcard names to catch dups
|
||||||
|
for len(rest) > 0 {
|
||||||
|
// Invariant: rest[0] == '/'.
|
||||||
|
rest = rest[1:]
|
||||||
|
off = len(s) - len(rest)
|
||||||
|
if len(rest) == 0 {
|
||||||
|
// Trailing slash.
|
||||||
|
p.segments = append(p.segments, segment{wild: true, multi: true})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i := strings.IndexByte(rest, '/')
|
||||||
|
if i < 0 {
|
||||||
|
i = len(rest)
|
||||||
|
}
|
||||||
|
var seg string
|
||||||
|
seg, rest = rest[:i], rest[i:]
|
||||||
|
if i := strings.IndexByte(seg, '{'); i < 0 {
|
||||||
|
// Literal.
|
||||||
|
seg = pathUnescape(seg)
|
||||||
|
p.segments = append(p.segments, segment{s: seg})
|
||||||
|
} else {
|
||||||
|
// Wildcard.
|
||||||
|
if i != 0 {
|
||||||
|
return nil, errors.New("bad wildcard segment (must start with '{')")
|
||||||
|
}
|
||||||
|
if seg[len(seg)-1] != '}' {
|
||||||
|
return nil, errors.New("bad wildcard segment (must end with '}')")
|
||||||
|
}
|
||||||
|
name := seg[1 : len(seg)-1]
|
||||||
|
if name == "$" {
|
||||||
|
if len(rest) != 0 {
|
||||||
|
return nil, errors.New("{$} not at end")
|
||||||
|
}
|
||||||
|
p.segments = append(p.segments, segment{s: "/"})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name, multi := strings.CutSuffix(name, "...")
|
||||||
|
if multi && len(rest) != 0 {
|
||||||
|
return nil, errors.New("{...} wildcard not at end")
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("empty wildcard")
|
||||||
|
}
|
||||||
|
if !isValidWildcardName(name) {
|
||||||
|
return nil, fmt.Errorf("bad wildcard name %q", name)
|
||||||
|
}
|
||||||
|
if seenNames[name] {
|
||||||
|
return nil, fmt.Errorf("duplicate wildcard name %q", name)
|
||||||
|
}
|
||||||
|
seenNames[name] = true
|
||||||
|
p.segments = append(p.segments, segment{s: name, wild: true, multi: multi})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidWildcardName(s string) bool {
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Valid Go identifier.
|
||||||
|
for i, c := range s {
|
||||||
|
if !unicode.IsLetter(c) && c != '_' && (i == 0 || !unicode.IsDigit(c)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathUnescape(path string) string {
|
||||||
|
u, err := url.PathUnescape(path)
|
||||||
|
if err != nil {
|
||||||
|
// Invalidly escaped path; use the original
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
@@ -336,6 +336,11 @@ type Request struct {
|
|||||||
// TINYGO: Add onEOF func for callback when response is fully read
|
// TINYGO: Add onEOF func for callback when response is fully read
|
||||||
// TINYGO: so we can close the connection.
|
// TINYGO: so we can close the connection.
|
||||||
onEOF func()
|
onEOF func()
|
||||||
|
|
||||||
|
// The following fields are for requests matched by ServeMux.
|
||||||
|
pat *pattern // the pattern that matched
|
||||||
|
matches []string // values for the matching wildcards in pat
|
||||||
|
otherValues map[string]string // for calls to SetPathValue that don't match a wildcard
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context returns the request's context. To change the context, use
|
// Context returns the request's context. To change the context, use
|
||||||
@@ -403,6 +408,20 @@ func (r *Request) Clone(ctx context.Context) *Request {
|
|||||||
r2.Form = cloneURLValues(r.Form)
|
r2.Form = cloneURLValues(r.Form)
|
||||||
r2.PostForm = cloneURLValues(r.PostForm)
|
r2.PostForm = cloneURLValues(r.PostForm)
|
||||||
r2.MultipartForm = cloneMultipartForm(r.MultipartForm)
|
r2.MultipartForm = cloneMultipartForm(r.MultipartForm)
|
||||||
|
|
||||||
|
// Copy matches and otherValues. See issue 61410.
|
||||||
|
if s := r.matches; s != nil {
|
||||||
|
s2 := make([]string, len(s))
|
||||||
|
copy(s2, s)
|
||||||
|
r2.matches = s2
|
||||||
|
}
|
||||||
|
if s := r.otherValues; s != nil {
|
||||||
|
s2 := make(map[string]string, len(s))
|
||||||
|
for k, v := range s {
|
||||||
|
s2[k] = v
|
||||||
|
}
|
||||||
|
r2.otherValues = s2
|
||||||
|
}
|
||||||
return r2
|
return r2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1456,3 +1475,47 @@ func (r *Request) requiresHTTP1() bool {
|
|||||||
return hasToken(r.Header.Get("Connection"), "upgrade") &&
|
return hasToken(r.Header.Get("Connection"), "upgrade") &&
|
||||||
ascii.EqualFold(r.Header.Get("Upgrade"), "websocket")
|
ascii.EqualFold(r.Header.Get("Upgrade"), "websocket")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PathValue returns the value for the named path wildcard in the [ServeMux] pattern
|
||||||
|
// that matched the request.
|
||||||
|
// It returns the empty string if the request was not matched against a pattern
|
||||||
|
// or there is no such wildcard in the pattern.
|
||||||
|
func (r *Request) PathValue(name string) string {
|
||||||
|
if i := r.patIndex(name); i >= 0 {
|
||||||
|
return r.matches[i]
|
||||||
|
}
|
||||||
|
return r.otherValues[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPathValue sets name to value, so that subsequent calls to r.PathValue(name)
|
||||||
|
// return value.
|
||||||
|
func (r *Request) SetPathValue(name, value string) {
|
||||||
|
if i := r.patIndex(name); i >= 0 {
|
||||||
|
r.matches[i] = value
|
||||||
|
} else {
|
||||||
|
if r.otherValues == nil {
|
||||||
|
r.otherValues = map[string]string{}
|
||||||
|
}
|
||||||
|
r.otherValues[name] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// patIndex returns the index of name in the list of named wildcards of the
|
||||||
|
// request's pattern, or -1 if there is no such name.
|
||||||
|
func (r *Request) patIndex(name string) int {
|
||||||
|
// The linear search seems expensive compared to a map, but just creating the map
|
||||||
|
// takes a lot of time, and most patterns will just have a couple of wildcards.
|
||||||
|
if r.pat == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for _, seg := range r.pat.segments {
|
||||||
|
if seg.wild && seg.s != "" {
|
||||||
|
if name == seg.s {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|||||||
+96
-1
@@ -2361,6 +2361,33 @@ func (mux *ServeMux) match(path string) (h Handler, pattern string) {
|
|||||||
return v.h, v.pattern
|
return v.h, v.pattern
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for longest valid match with path placeholders (like /users/{id})
|
||||||
|
// First collect all potential matching patterns
|
||||||
|
var matchingPatterns []string
|
||||||
|
for registeredPattern := range mux.m {
|
||||||
|
// Skip patterns ending with "/" as they are handled separately
|
||||||
|
if registeredPattern[len(registeredPattern)-1] == '/' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the pattern contains placeholders and could match the path
|
||||||
|
if strings.Contains(registeredPattern, "{") && strings.Contains(registeredPattern, "}") {
|
||||||
|
if patternCouldMatch(registeredPattern, path) {
|
||||||
|
matchingPatterns = append(matchingPatterns, registeredPattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the longest matching pattern
|
||||||
|
if len(matchingPatterns) > 0 {
|
||||||
|
sort.Slice(matchingPatterns, func(i, j int) bool {
|
||||||
|
return len(matchingPatterns[i]) > len(matchingPatterns[j])
|
||||||
|
})
|
||||||
|
|
||||||
|
pattern = matchingPatterns[0]
|
||||||
|
return mux.m[pattern].h, pattern
|
||||||
|
}
|
||||||
|
|
||||||
// Check for longest valid match. mux.es contains all patterns
|
// Check for longest valid match. mux.es contains all patterns
|
||||||
// that end in / sorted from longest to shortest.
|
// that end in / sorted from longest to shortest.
|
||||||
for _, e := range mux.es {
|
for _, e := range mux.es {
|
||||||
@@ -2371,6 +2398,33 @@ func (mux *ServeMux) match(path string) (h Handler, pattern string) {
|
|||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// patternCouldMatch checks if a pattern with placeholders could match the given path.
|
||||||
|
// It handles patterns like /users/{id}/orders/{orderId}.
|
||||||
|
func patternCouldMatch(pattern, path string) bool {
|
||||||
|
patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
|
||||||
|
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
|
|
||||||
|
// If they have different number of parts, they can't match
|
||||||
|
if len(patternParts) != len(pathParts) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each part
|
||||||
|
for i, patternPart := range patternParts {
|
||||||
|
// If it's a placeholder, it matches anything
|
||||||
|
if strings.HasPrefix(patternPart, "{") && strings.HasSuffix(patternPart, "}") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a placeholder, it must match exactly
|
||||||
|
if patternPart != pathParts[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// redirectToPathSlash determines if the given path needs appending "/" to it.
|
// redirectToPathSlash determines if the given path needs appending "/" to it.
|
||||||
// This occurs when a handler for path + "/" was already registered, but
|
// This occurs when a handler for path + "/" was already registered, but
|
||||||
// not for path itself. If the path needs appending to, it creates a new
|
// not for path itself. If the path needs appending to, it creates a new
|
||||||
@@ -2480,6 +2534,36 @@ func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractPathValues extracts path parameter values from a URL path based on a pattern.
|
||||||
|
// It handles patterns with placeholders like /users/{id}/orders/{orderId}
|
||||||
|
// and returns a map of parameter names to their values.
|
||||||
|
func extractPathValues(pattern, path string) map[string]string {
|
||||||
|
pathValues := make(map[string]string)
|
||||||
|
|
||||||
|
// Split both pattern and path by "/"
|
||||||
|
patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
|
||||||
|
pathParts := strings.Split(strings.Trim(path, "/"), "/")
|
||||||
|
|
||||||
|
// If they have different lengths, they can't match (unless the pattern has a wildcard)
|
||||||
|
if len(patternParts) != len(pathParts) {
|
||||||
|
return pathValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare each part
|
||||||
|
for i, patternPart := range patternParts {
|
||||||
|
if strings.HasPrefix(patternPart, "{") && strings.HasSuffix(patternPart, "}") {
|
||||||
|
// This is a placeholder like {id}
|
||||||
|
paramName := patternPart[1 : len(patternPart)-1]
|
||||||
|
pathValues[paramName] = pathParts[i]
|
||||||
|
} else if patternPart != pathParts[i] {
|
||||||
|
// If a non-placeholder part doesn't match, return empty
|
||||||
|
return make(map[string]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathValues
|
||||||
|
}
|
||||||
|
|
||||||
// ServeHTTP dispatches the request to the handler whose
|
// ServeHTTP dispatches the request to the handler whose
|
||||||
// pattern most closely matches the request URL.
|
// pattern most closely matches the request URL.
|
||||||
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
|
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
|
||||||
@@ -2490,7 +2574,18 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
|
|||||||
w.WriteHeader(StatusBadRequest)
|
w.WriteHeader(StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h, _ := mux.Handler(r)
|
h, pattern := mux.Handler(r)
|
||||||
|
|
||||||
|
// Extract path values for patterns that contain placeholders like {id}
|
||||||
|
if strings.Contains(pattern, "{") && strings.Contains(pattern, "}") {
|
||||||
|
pathValues := extractPathValues(pattern, r.URL.Path)
|
||||||
|
|
||||||
|
// Set path values in the request
|
||||||
|
for name, value := range pathValues {
|
||||||
|
r.SetPathValue(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user