123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- 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'
- }
|