view cmd/sievemgr/internal/config/scanner.go @ 22:fc5e6970a0d5 default tip

Add support for specifying an authorization identity on the command line
author Guido Berhoerster <guido+sievemgr@berhoerster.name>
date Wed, 17 Feb 2021 07:50:55 +0100
parents 4dff4c3f0fbb
children
line wrap: on
line source

// Copyright (C) 2020 Guido Berhoerster <guido+sievemgr@berhoerster.name>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package config

import (
	"bufio"
	"fmt"
	"io"
	"strings"
	"unicode"
)

type token int

const (
	tokenEOF token = -(iota + 1)
	tokenIllegal
	tokenIdent
	tokenNumber
	tokenString
)

func (tok token) String() string {
	switch tok {
	case tokenIllegal:
		return "illegal"
	case tokenEOF:
		return "EOF"
	case tokenIdent:
		return "identifier"
	case tokenNumber:
		return "number"
	case tokenString:
		return "string"
	default:
		return "unknown"
	}
}

func isWhitespaceRune(r rune) bool {
	return r == ' ' || r == '\t' || r == '\r' || r == '\n'
}

func isIdentRune(r rune) bool {
	return r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z'
}

func isNumberRune(r rune) bool {
	return r >= '0' && r <= '9'
}

type scanner struct {
	br       *bufio.Reader
	line     int  // line number for error messages
	r        rune // last read rune
	rdSize   int  // last read size
	rdOffset int  // offset from beginning of file
}

func newScanner(br *bufio.Reader) *scanner {
	return &scanner{line: 1, br: br}
}

func (s *scanner) read() error {
	var size int
again:
	r, size, err := s.br.ReadRune()
	if err != nil {
		return err
	}

	// skip over BOM at the beginning of the file
	if r == bom && s.rdOffset == 0 {
		s.rdOffset += size
		goto again
	}

	if s.r == '\n' {
		s.line++
	}

	s.r = r
	s.rdOffset += size
	s.rdSize = size

	if r == unicode.ReplacementChar && size == 1 {
		return fmt.Errorf("illegal UTF-8 sequence")
	} else if r == 0 {
		return fmt.Errorf("illegal nul byte")
	}
	return nil
}

func (s *scanner) unread() {
	if s.br.UnreadRune() != nil {
		return
	}

	if b, _ := s.br.Peek(1); b[0] == '\n' {
		// moved back to the previous line
		s.line--
	}

	s.r = 0
	s.rdOffset -= s.rdSize
	s.rdSize = 0
}

func (s *scanner) skipWhitespace() error {
	for {
		if err := s.read(); err == io.EOF {
			break
		} else if err != nil {
			return err
		} else if !isWhitespaceRune(s.r) {
			s.unread()
			break
		}
	}
	return nil
}

func (s *scanner) scanIdent() (token, string, error) {
	var sb strings.Builder
	for {
		if err := s.read(); err == io.EOF {
			break
		} else if err != nil {
			return tokenIllegal, "", err
		}
		if isIdentRune(s.r) {
			sb.WriteRune(s.r)
		} else if isWhitespaceRune(s.r) {
			s.unread()
			break
		} else {
			return tokenIllegal, "", fmt.Errorf("illegal rune in identifier: %q", s.r)
		}
	}
	if sb.Len() == 0 {
		return tokenIllegal, "", fmt.Errorf("expected identifier, got %q", s.r)
	}
	return tokenIdent, sb.String(), nil
}

func (s *scanner) scanNumber() (token, string, error) {
	var sb strings.Builder
	for {
		if err := s.read(); err == io.EOF {
			break
		} else if err != nil {
			return tokenIllegal, "", err
		}
		if isNumberRune(s.r) {
			sb.WriteRune(s.r)
		} else if isWhitespaceRune(s.r) {
			s.unread()
			break
		} else {
			return tokenIllegal, "", fmt.Errorf("illegal rune in number: %q", s.r)
		}
	}
	if sb.Len() == 0 {
		return tokenIllegal, "", fmt.Errorf("expected number, got %q", s.r)
	}
	return tokenNumber, sb.String(), nil
}

func (s *scanner) scanString() (token, string, error) {
	if err := s.read(); err == io.EOF {
		return tokenIllegal, "", fmt.Errorf("unexpected EOF")
	} else if err != nil {
		return tokenIllegal, "", err
	}

	if s.r != '"' {
		return tokenIllegal, "",
			fmt.Errorf("expected '\"', got %q", s.r)
	}

	var sb strings.Builder
	var inEscape bool
	for {
		if err := s.read(); err == io.EOF {
			return tokenIllegal, "", fmt.Errorf("unterminated string")
		} else if err != nil {
			return tokenIllegal, "", err
		}

		if s.r == '\\' && !inEscape {
			inEscape = true
		} else if s.r == '"' && !inEscape {
			break
		} else {
			sb.WriteRune(s.r)
			inEscape = false
		}
	}

	return tokenString, sb.String(), nil
}

func (s *scanner) scan() (token, string, error) {
	if err := s.skipWhitespace(); err != nil {
		return tokenIllegal, "", err
	}

	if err := s.read(); err == io.EOF {
		return tokenEOF, "", nil
	} else if err != nil {
		return tokenIllegal, "", err
	}
	r := s.r
	s.unread()

	switch {
	case isIdentRune(r):
		return s.scanIdent()
	case isNumberRune(r):
		return s.scanNumber()
	case r == '"':
		return s.scanString()
	}
	return tokenIllegal, string(r), nil
}