WIP: More advanced action parsing
parent
53646737e8
commit
574e72a974
|
@ -0,0 +1,207 @@
|
||||||
|
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'
|
||||||
|
}
|
|
@ -54,7 +54,7 @@ func parseActions(s string) (actions []*action, err error) {
|
||||||
if strings.HasPrefix(s, "[") {
|
if strings.HasPrefix(s, "[") {
|
||||||
actions, err = parseActionsFromJSON(s)
|
actions, err = parseActionsFromJSON(s)
|
||||||
} else {
|
} else {
|
||||||
actions, err = parseActionsFromSimple(s)
|
actions, err = parseActionsFromSimpleNew(s)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -79,4 +79,31 @@ func TestParseActions(t *testing.T) {
|
||||||
require.Equal(t, 2, len(actions[0].Headers))
|
require.Equal(t, 2, len(actions[0].Headers))
|
||||||
require.Equal(t, "application/json", actions[0].Headers["Content-Type"])
|
require.Equal(t, "application/json", actions[0].Headers["Content-Type"])
|
||||||
require.Equal(t, "Basic sdasffsf", actions[0].Headers["Authorization"])
|
require.Equal(t, "Basic sdasffsf", actions[0].Headers["Authorization"])
|
||||||
|
|
||||||
|
actions, err = parseActions(`action=http, "Look ma, \"quotes\"; and semicolons", url=http://example.com`)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, 1, len(actions))
|
||||||
|
require.Equal(t, "http", actions[0].Action)
|
||||||
|
require.Equal(t, `Look ma, \"quotes\"; and semicolons`, actions[0].Label)
|
||||||
|
require.Equal(t, `http://example.com`, actions[0].URL)
|
||||||
|
|
||||||
|
actions, err = parseActions(`label="Out of order!" , action="http", url=http://example.com`)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, 1, len(actions))
|
||||||
|
require.Equal(t, "http", actions[0].Action)
|
||||||
|
require.Equal(t, `Out of order!`, actions[0].Label)
|
||||||
|
require.Equal(t, `http://example.com`, actions[0].URL)
|
||||||
|
|
||||||
|
actions, err = parseActions(`label="Out of order!" x, action="http", url=http://example.com`)
|
||||||
|
require.EqualError(t, err, "unexpected character 'x' at position 22")
|
||||||
|
|
||||||
|
actions, err = parseActions(`label="", action="http", url=http://example.com`)
|
||||||
|
require.EqualError(t, err, "invalid request: actions invalid, parameter 'label' is required")
|
||||||
|
|
||||||
|
actions, err = parseActions(`label=, action="http", url=http://example.com`)
|
||||||
|
require.EqualError(t, err, "invalid request: actions invalid, parameter 'label' is required")
|
||||||
|
|
||||||
|
actions, err = parseActions(`label="xx", action="http", url=http://example.com, what is this anyway`)
|
||||||
|
require.EqualError(t, err, "invalid request: actions invalid, term 'what is this anyway' unknown")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue