2021-05-14 15:23:33 +00:00
|
|
|
package uri
|
2018-04-26 11:03:44 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2018-09-29 12:39:12 +00:00
|
|
|
"strings"
|
2018-04-26 11:03:44 +00:00
|
|
|
)
|
|
|
|
|
2021-05-14 15:23:33 +00:00
|
|
|
// === WILDCARDS ===
|
|
|
|
//
|
|
|
|
// The star '*' -> matches 0 or 1 slash-bounded string
|
|
|
|
// The multi star '**' -> matches 0 or more slash-separated strings
|
|
|
|
// The dot '.' -> matches 1 slash-bounded string
|
|
|
|
// The multi dot '..' -> matches 1 or more slash-separated strings
|
|
|
|
//
|
|
|
|
// === SCHEME POLICY ===
|
|
|
|
//
|
|
|
|
// - The last '/' is optional
|
|
|
|
// - Any '**' at the very end will match anything that starts with the given prefix
|
|
|
|
//
|
|
|
|
// === LIMITATIONS ==
|
|
|
|
//
|
|
|
|
// - A scheme must begin with '/'
|
|
|
|
// - A scheme cannot contain something else than a STRING or WILDCARD between 2 '/' separators
|
|
|
|
// - A scheme STRING cannot contain the symbols '/' as a character
|
|
|
|
// - A scheme STRING containing '*' or '.' characters will be treating as STRING only
|
|
|
|
// - A maximum of 16 slash-separated matchers (STRING or WILDCARD) are allowed
|
|
|
|
|
|
|
|
const maxMatch = 16
|
|
|
|
|
|
|
|
// Represents an URI matcher
|
|
|
|
type matcher struct {
|
|
|
|
pat string // pattern to match (empty if wildcard)
|
|
|
|
req bool // whether it is required
|
|
|
|
mul bool // whether multiple matches are allowed
|
|
|
|
|
|
|
|
buf []string // matched content (when matching)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scheme represents an URI scheme
|
|
|
|
type Scheme []*matcher
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// FromString builds an URI scheme from a string pattern
|
2021-05-14 15:23:33 +00:00
|
|
|
func FromString(s string) (*Scheme, error) {
|
2021-06-15 22:04:09 +00:00
|
|
|
// handle '/' at the start
|
2021-05-14 15:23:33 +00:00
|
|
|
if len(s) < 1 || s[0] != '/' {
|
2021-06-15 20:13:36 +00:00
|
|
|
return nil, fmt.Errorf("invalid URI; must start with '/'")
|
2021-05-14 15:23:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
parts := strings.Split(s, "/")
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// check max match size
|
2021-05-14 15:23:33 +00:00
|
|
|
if len(parts)-2 > maxMatch {
|
|
|
|
for i, p := range parts {
|
|
|
|
fmt.Printf("%d: '%s'\n", i, p)
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("URI must not exceed %d slash-separated components, got %d", maxMatch, len(parts))
|
|
|
|
}
|
|
|
|
|
|
|
|
sch, err := buildScheme(parts)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
opti, err := sch.optimise()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &opti, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// Match returns whether the given URI is matched by the scheme
|
|
|
|
func (s Scheme) Match(uri string) bool {
|
2021-05-14 15:23:33 +00:00
|
|
|
if len(s) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// check for string match
|
|
|
|
clearURI, match := s.matchString(uri)
|
2021-05-14 15:23:33 +00:00
|
|
|
if !match {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// check for non-string match (wildcards)
|
|
|
|
return s.matchWildcards(clearURI)
|
2021-05-14 15:23:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetMatch returns the indexed match (excluding string matchers)
|
|
|
|
func (s Scheme) GetMatch(n uint8) ([]string, error) {
|
|
|
|
if n > uint8(len(s)) {
|
2021-06-15 20:13:36 +00:00
|
|
|
return nil, fmt.Errorf("index out of range")
|
2021-05-14 15:23:33 +00:00
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// iterate to find index (exclude strings)
|
|
|
|
matches := -1
|
2021-05-14 15:23:33 +00:00
|
|
|
for _, m := range s {
|
|
|
|
if len(m.pat) > 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
matches++
|
2021-05-14 15:23:33 +00:00
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// expected index -> return matches
|
|
|
|
if uint8(matches) == n {
|
2021-05-14 15:23:33 +00:00
|
|
|
return m.buf, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// nothing found -> return empty set
|
|
|
|
return nil, fmt.Errorf("index out of range (max: %d)", matches)
|
2021-05-14 15:23:33 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAllMatch returns all the indexed match (excluding string matchers)
|
|
|
|
func (s Scheme) GetAllMatch() [][]string {
|
|
|
|
match := make([][]string, 0, len(s))
|
|
|
|
|
|
|
|
for _, m := range s {
|
|
|
|
if len(m.pat) > 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
match = append(match, m.buf)
|
|
|
|
}
|
|
|
|
return match
|
|
|
|
}
|
|
|
|
|
2018-04-26 11:03:44 +00:00
|
|
|
// buildScheme builds a 'basic' scheme
|
|
|
|
// from a pattern string
|
|
|
|
func buildScheme(ss []string) (Scheme, error) {
|
|
|
|
sch := make(Scheme, 0, maxMatch)
|
|
|
|
|
|
|
|
for _, s := range ss {
|
2018-09-29 12:39:12 +00:00
|
|
|
if len(s) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2018-04-26 11:03:44 +00:00
|
|
|
|
2021-05-14 15:19:02 +00:00
|
|
|
m := &matcher{}
|
2018-04-26 11:03:44 +00:00
|
|
|
|
|
|
|
switch s {
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// card: 0, N
|
2018-09-29 12:39:12 +00:00
|
|
|
case "**":
|
|
|
|
m.req = false
|
|
|
|
m.mul = true
|
|
|
|
sch = append(sch, m)
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// card: 1, N
|
2018-09-29 12:39:12 +00:00
|
|
|
case "..":
|
|
|
|
m.req = true
|
|
|
|
m.mul = true
|
|
|
|
sch = append(sch, m)
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// card: 0, 1
|
2018-09-29 12:39:12 +00:00
|
|
|
case "*":
|
|
|
|
m.req = false
|
|
|
|
m.mul = false
|
|
|
|
sch = append(sch, m)
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// card: 1
|
2018-09-29 12:39:12 +00:00
|
|
|
case ".":
|
|
|
|
m.req = true
|
|
|
|
m.mul = false
|
|
|
|
sch = append(sch, m)
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// card: 1, literal string
|
2018-09-29 12:39:12 +00:00
|
|
|
default:
|
|
|
|
m.req = true
|
|
|
|
m.mul = false
|
|
|
|
m.pat = fmt.Sprintf("/%s", s)
|
|
|
|
sch = append(sch, m)
|
2018-04-26 11:03:44 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return sch, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// optimise optimised the scheme for further parsing
|
|
|
|
func (s Scheme) optimise() (Scheme, error) {
|
|
|
|
if len(s) <= 1 {
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// init reshifted scheme
|
2018-04-26 11:03:44 +00:00
|
|
|
rshift := make(Scheme, 0, maxMatch)
|
|
|
|
rshift = append(rshift, s[0])
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// iterate over matchers
|
2018-09-29 12:39:12 +00:00
|
|
|
for p, i, l := 0, 1, len(s); i < l; i++ {
|
2018-04-26 11:03:44 +00:00
|
|
|
|
|
|
|
pre, cur := s[p], s[i]
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// merge: 2 following literals
|
2018-04-26 11:03:44 +00:00
|
|
|
if len(pre.pat) > 0 && len(cur.pat) > 0 {
|
|
|
|
// merge strings into previous
|
|
|
|
pre.pat = fmt.Sprintf("%s%s", pre.pat, cur.pat)
|
|
|
|
|
|
|
|
// delete current
|
|
|
|
s[i] = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// increment previous (only if current is not nul)
|
|
|
|
if s[i] != nil {
|
|
|
|
rshift = append(rshift, s[i])
|
|
|
|
p = i
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return rshift, nil
|
2018-04-26 20:15:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// matchString checks the STRING matchers from an URI
|
2021-06-15 22:04:09 +00:00
|
|
|
// - returns a boolean : false when not matching, true eitherway
|
|
|
|
// - returns a cleared uri, without STRING data
|
2018-04-26 20:15:09 +00:00
|
|
|
func (s Scheme) matchString(uri string) (string, bool) {
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
var (
|
|
|
|
clearedInput = uri
|
|
|
|
minOffset = 0
|
|
|
|
)
|
2018-04-26 20:15:09 +00:00
|
|
|
|
|
|
|
for _, m := range s {
|
|
|
|
ls := len(m.pat)
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// ignore no STRING match
|
2018-09-29 12:39:12 +00:00
|
|
|
if ls == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2018-04-26 20:15:09 +00:00
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// get offset in URI (else -1)
|
|
|
|
off := strings.Index(clearedInput, m.pat)
|
2018-09-29 12:39:12 +00:00
|
|
|
if off < 0 {
|
|
|
|
return "", false
|
|
|
|
}
|
2018-04-26 20:15:09 +00:00
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// fail on invalid offset range
|
|
|
|
if off < minOffset {
|
2018-09-29 12:39:12 +00:00
|
|
|
return "", false
|
|
|
|
}
|
2018-04-26 20:15:09 +00:00
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// check for trailing '/'
|
2018-04-26 20:15:09 +00:00
|
|
|
hasSlash := 0
|
2021-06-15 22:04:09 +00:00
|
|
|
if off+ls < len(clearedInput) && clearedInput[off+ls] == '/' {
|
2018-04-26 20:15:09 +00:00
|
|
|
hasSlash = 1
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// remove the current string (+trailing slash) from the URI
|
|
|
|
beg, end := clearedInput[:off], clearedInput[off+ls+hasSlash:]
|
|
|
|
clearedInput = fmt.Sprintf("%s\a/%s", beg, end) // separate matches with a '\a' character
|
2018-04-26 20:15:09 +00:00
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// update offset range
|
|
|
|
// +2 slash separators
|
2018-09-29 12:39:12 +00:00
|
|
|
// -1 because strings begin with 1 slash already
|
2021-06-15 22:04:09 +00:00
|
|
|
minOffset = len(beg) + 2 - 1
|
2018-04-26 20:15:09 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// if exists, remove trailing '/'
|
|
|
|
if clearedInput[len(clearedInput)-1] == '/' {
|
|
|
|
clearedInput = clearedInput[:len(clearedInput)-1]
|
2018-04-26 20:15:09 +00:00
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// if exists, remove trailing '\a'
|
|
|
|
if clearedInput[len(clearedInput)-1] == '\a' {
|
|
|
|
clearedInput = clearedInput[:len(clearedInput)-1]
|
2018-04-26 20:15:09 +00:00
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
return clearedInput, true
|
2018-04-26 20:15:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// matchWildcards check the WILCARDS (non-string) matchers from
|
|
|
|
// a cleared URI. it returns if the string matches
|
|
|
|
// + it sets the matchers buffers for later extraction
|
|
|
|
func (s Scheme) matchWildcards(clear string) bool {
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// extract wildcards (ref)
|
2018-04-26 20:15:09 +00:00
|
|
|
wildcards := make(Scheme, 0, maxMatch)
|
|
|
|
|
|
|
|
for _, m := range s {
|
|
|
|
if len(m.pat) == 0 {
|
|
|
|
m.buf = nil // flush buffers
|
|
|
|
wildcards = append(wildcards, m)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(wildcards) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// break uri by '\a' characters
|
2018-04-26 20:15:09 +00:00
|
|
|
matches := strings.Split(clear, "\a")[1:]
|
|
|
|
|
|
|
|
for n, match := range matches {
|
2021-06-15 22:04:09 +00:00
|
|
|
// no more matcher
|
2018-04-26 20:15:09 +00:00
|
|
|
if n >= len(wildcards) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// from index 1 because it begins with '/'
|
|
|
|
data := strings.Split(match, "/")[1:]
|
2018-04-26 20:15:09 +00:00
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// missing required
|
2018-04-26 20:15:09 +00:00
|
|
|
if wildcards[n].req && len(data) < 1 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-06-15 22:04:09 +00:00
|
|
|
// if not multi but got multi
|
2018-04-26 20:15:09 +00:00
|
|
|
if !wildcards[n].mul && len(data) > 1 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
wildcards[n].buf = data
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
2018-09-29 12:39:12 +00:00
|
|
|
}
|