// TINYGO: The following is copied and modified from Go 1.26.2 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 }