ntfy/server/actions_parse.go

208 lines
4.7 KiB
Go
Raw Normal View History

2022-04-27 05:07:31 +02:00
package server
import (
"errors"
"fmt"
"heckel.io/ntfy/util"
"regexp"
"strings"
"unicode/utf8"
)
// Heavily inspired by https://go.dev/src/text/template/parse/lex.go
// And thanks to Rob Pike (for Go, but also) for https://www.youtube.com/watch?v=HxaD_trXwRE
// action=view, label="Look ma, commas and \"quotes\" too", url=https://..
// "Look ma, a button",
// Look ma a button
// label=Look ma a=button
// label="Look ma, a button"
// "Look ma, \"quotes\""
// label="Look ma, \"quotes\""
// label=,
func parseActionsFromSimpleNew(s string) ([]*action, error) {
if !utf8.ValidString(s) {
return nil, errors.New("invalid string")
}
parser := &actionParser{
pos: 0,
input: s,
}
return parser.Parse()
}
type actionParser struct {
input string
pos int
}
const eof = rune(0)
func (p *actionParser) Parse() ([]*action, error) {
println("------------------------")
actions := make([]*action, 0)
for !p.eof() {
a, err := p.parseAction()
if err != nil {
return nil, err
} else if a == nil {
return actions, err
}
actions = append(actions, a)
}
return actions, nil
}
func (p *actionParser) parseAction() (*action, error) {
println("parseAction")
newAction := &action{
Headers: make(map[string]string),
Extras: make(map[string]string),
}
section := 0
for {
key, value, last, err := p.parseSection()
fmt.Printf("--> key=%s, value=%s, last=%t, err=%#v\n", key, value, last, err)
if err != nil {
return nil, err
} else if key == "" && section == 0 {
key = "action"
} else if key == "" && section == 1 {
key = "label"
} else if key == "" && section == 2 && util.InStringList([]string{"view", "http"}, newAction.Action) {
key = "url"
} else if key == "" {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "term '%s' unknown", value)
}
if strings.HasPrefix(key, "headers.") {
newAction.Headers[strings.TrimPrefix(key, "headers.")] = value
} else if strings.HasPrefix(key, "extras.") {
newAction.Extras[strings.TrimPrefix(key, "extras.")] = value
} else {
switch strings.ToLower(key) {
case "action":
newAction.Action = value
case "label":
newAction.Label = value
case "clear":
lvalue := strings.ToLower(value)
if !util.InStringList([]string{"true", "yes", "1", "false", "no", "0"}, lvalue) {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "'clear=%s' not allowed", value)
}
newAction.Clear = lvalue == "true" || lvalue == "yes" || lvalue == "1"
case "url":
newAction.URL = value
case "method":
newAction.Method = value
case "body":
newAction.Body = value
default:
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "key '%s' unknown", key)
}
}
p.slurpSpaces()
if last {
return newAction, nil
}
section++
}
}
func (p *actionParser) parseSection() (key string, value string, last bool, err error) {
fmt.Printf("parseSection, pos=%d, len(input)=%d, input[pos:]=%s\n", p.pos, len(p.input), p.input[p.pos:])
p.slurpSpaces()
key = p.parseKey()
r, w := p.peek()
if r == eof || r == ';' || r == ',' {
p.pos += w
last = r == ';' || r == eof
return
} else if r == '"' {
value, last, err = p.parseQuotedValue()
return
}
value, last = p.parseValue()
return
}
func (p *actionParser) parseValue() (value string, last bool) {
start := p.pos
for {
r, w := p.peek()
if r == eof || r == ';' || r == ',' {
last = r == ';' || r == eof
value = p.input[start:p.pos]
p.pos += w
return
}
p.pos += w
}
}
func (p *actionParser) parseQuotedValue() (value string, last bool, err error) {
p.pos++
start := p.pos
var prev rune
for {
r, w := p.peek()
if r == eof {
err = errors.New("unexpected end of input")
return
} else if r == '"' && prev != '\\' {
value = p.input[start:p.pos]
p.pos += w
// Advance until after "," or ";"
p.slurpSpaces()
r, w := p.peek()
last = r == ';' || r == eof
if r != eof && r != ';' && r != ',' {
err = fmt.Errorf("unexpected character '%c' at position %d", r, p.pos)
return
}
p.pos += w
return
}
prev = r
p.pos += w
}
}
var keyRegex = regexp.MustCompile(`^[-.\w]+=`)
func (p *actionParser) parseKey() string {
key := keyRegex.FindString(p.input[p.pos:])
if key != "" {
p.pos += len(key)
return key[:len(key)-1]
}
return key
}
func (p *actionParser) peek() (rune, int) {
if p.pos >= len(p.input) {
return eof, 0
}
return utf8.DecodeRuneInString(p.input[p.pos:])
}
func (p *actionParser) eof() bool {
return p.pos >= len(p.input)
}
func (p *actionParser) slurpSpaces() {
for {
r, w := p.peek()
if r == eof || !isSpace(r) {
return
}
p.pos += w
}
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t' || r == '\r' || r == '\n'
}