mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-05-06 22:18:28 +00:00
Update Go AWS SDK to the latest version
This commit is contained in:
committed by
Andrey Smirnov
parent
d08be990ef
commit
94a72b23ff
+120
@@ -0,0 +1,120 @@
|
||||
package ini
|
||||
|
||||
// ASTKind represents different states in the parse table
|
||||
// and the type of AST that is being constructed
|
||||
type ASTKind int
|
||||
|
||||
// ASTKind* is used in the parse table to transition between
|
||||
// the different states
|
||||
const (
|
||||
ASTKindNone = ASTKind(iota)
|
||||
ASTKindStart
|
||||
ASTKindExpr
|
||||
ASTKindEqualExpr
|
||||
ASTKindStatement
|
||||
ASTKindSkipStatement
|
||||
ASTKindExprStatement
|
||||
ASTKindSectionStatement
|
||||
ASTKindNestedSectionStatement
|
||||
ASTKindCompletedNestedSectionStatement
|
||||
ASTKindCommentStatement
|
||||
ASTKindCompletedSectionStatement
|
||||
)
|
||||
|
||||
func (k ASTKind) String() string {
|
||||
switch k {
|
||||
case ASTKindNone:
|
||||
return "none"
|
||||
case ASTKindStart:
|
||||
return "start"
|
||||
case ASTKindExpr:
|
||||
return "expr"
|
||||
case ASTKindStatement:
|
||||
return "stmt"
|
||||
case ASTKindSectionStatement:
|
||||
return "section_stmt"
|
||||
case ASTKindExprStatement:
|
||||
return "expr_stmt"
|
||||
case ASTKindCommentStatement:
|
||||
return "comment"
|
||||
case ASTKindNestedSectionStatement:
|
||||
return "nested_section_stmt"
|
||||
case ASTKindCompletedSectionStatement:
|
||||
return "completed_stmt"
|
||||
case ASTKindSkipStatement:
|
||||
return "skip"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// AST interface allows us to determine what kind of node we
|
||||
// are on and casting may not need to be necessary.
|
||||
//
|
||||
// The root is always the first node in Children
|
||||
type AST struct {
|
||||
Kind ASTKind
|
||||
Root Token
|
||||
RootToken bool
|
||||
Children []AST
|
||||
}
|
||||
|
||||
func newAST(kind ASTKind, root AST, children ...AST) AST {
|
||||
return AST{
|
||||
Kind: kind,
|
||||
Children: append([]AST{root}, children...),
|
||||
}
|
||||
}
|
||||
|
||||
func newASTWithRootToken(kind ASTKind, root Token, children ...AST) AST {
|
||||
return AST{
|
||||
Kind: kind,
|
||||
Root: root,
|
||||
RootToken: true,
|
||||
Children: children,
|
||||
}
|
||||
}
|
||||
|
||||
// AppendChild will append to the list of children an AST has.
|
||||
func (a *AST) AppendChild(child AST) {
|
||||
a.Children = append(a.Children, child)
|
||||
}
|
||||
|
||||
// GetRoot will return the root AST which can be the first entry
|
||||
// in the children list or a token.
|
||||
func (a *AST) GetRoot() AST {
|
||||
if a.RootToken {
|
||||
return *a
|
||||
}
|
||||
|
||||
if len(a.Children) == 0 {
|
||||
return AST{}
|
||||
}
|
||||
|
||||
return a.Children[0]
|
||||
}
|
||||
|
||||
// GetChildren will return the current AST's list of children
|
||||
func (a *AST) GetChildren() []AST {
|
||||
if len(a.Children) == 0 {
|
||||
return []AST{}
|
||||
}
|
||||
|
||||
if a.RootToken {
|
||||
return a.Children
|
||||
}
|
||||
|
||||
return a.Children[1:]
|
||||
}
|
||||
|
||||
// SetChildren will set and override all children of the AST.
|
||||
func (a *AST) SetChildren(children []AST) {
|
||||
if a.RootToken {
|
||||
a.Children = children
|
||||
} else {
|
||||
a.Children = append(a.Children[:1], children...)
|
||||
}
|
||||
}
|
||||
|
||||
// Start is used to indicate the starting state of the parse table.
|
||||
var Start = newAST(ASTKindStart, AST{})
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
section = `[default]
|
||||
region = us-west-2
|
||||
credential_source = Ec2InstanceMetadata
|
||||
s3 =
|
||||
foo=bar
|
||||
bar=baz
|
||||
output = json
|
||||
|
||||
[assumerole]
|
||||
output = json
|
||||
region = us-west-2
|
||||
`
|
||||
)
|
||||
|
||||
func BenchmarkINIParser(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseBytes([]byte(section))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTokenize(b *testing.B) {
|
||||
lexer := iniLexer{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
lexer.tokenize([]byte(section))
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ini
|
||||
|
||||
var commaRunes = []rune(",")
|
||||
|
||||
func isComma(b rune) bool {
|
||||
return b == ','
|
||||
}
|
||||
|
||||
func newCommaToken() Token {
|
||||
return newToken(TokenComma, commaRunes, NoneType)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package ini
|
||||
|
||||
// isComment will return whether or not the next byte(s) is a
|
||||
// comment.
|
||||
func isComment(b []rune) bool {
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch b[0] {
|
||||
case ';':
|
||||
return true
|
||||
case '#':
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// newCommentToken will create a comment token and
|
||||
// return how many bytes were read.
|
||||
func newCommentToken(b []rune) (Token, int, error) {
|
||||
i := 0
|
||||
for ; i < len(b); i++ {
|
||||
if b[i] == '\n' {
|
||||
break
|
||||
}
|
||||
|
||||
if len(b)-i > 2 && b[i] == '\r' && b[i+1] == '\n' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return newToken(TokenComment, b[:i], NoneType), i, nil
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Package ini is an LL(1) parser for configuration files.
|
||||
//
|
||||
// Example:
|
||||
// sections, err := ini.OpenFile("/path/to/file")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
//
|
||||
// profile := "foo"
|
||||
// section, ok := sections.GetSection(profile)
|
||||
// if !ok {
|
||||
// fmt.Printf("section %q could not be found", profile)
|
||||
// }
|
||||
//
|
||||
// Below is the BNF that describes this parser
|
||||
// Grammar:
|
||||
// stmt -> value stmt'
|
||||
// stmt' -> epsilon | op stmt
|
||||
// value -> number | string | boolean | quoted_string
|
||||
//
|
||||
// section -> [ section'
|
||||
// section' -> value section_close
|
||||
// section_close -> ]
|
||||
//
|
||||
// SkipState will skip (NL WS)+
|
||||
//
|
||||
// comment -> # comment' | ; comment'
|
||||
// comment' -> epsilon | value
|
||||
package ini
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package ini
|
||||
|
||||
// emptyToken is used to satisfy the Token interface
|
||||
var emptyToken = newToken(TokenNone, []rune{}, NoneType)
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package ini
|
||||
|
||||
// newExpression will return an expression AST.
|
||||
// Expr represents an expression
|
||||
//
|
||||
// grammar:
|
||||
// expr -> string | number
|
||||
func newExpression(tok Token) AST {
|
||||
return newASTWithRootToken(ASTKindExpr, tok)
|
||||
}
|
||||
|
||||
func newEqualExpr(left AST, tok Token) AST {
|
||||
return newASTWithRootToken(ASTKindEqualExpr, tok, left)
|
||||
}
|
||||
|
||||
// EqualExprKey will return a LHS value in the equal expr
|
||||
func EqualExprKey(ast AST) string {
|
||||
children := ast.GetChildren()
|
||||
if len(children) == 0 || ast.Kind != ASTKindEqualExpr {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(children[0].Root.Raw())
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// +build gofuzz
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
b := bytes.NewReader(data)
|
||||
|
||||
if _, err := Parse(b); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// +build fuzz
|
||||
|
||||
// fuzz test data is stored in Amazon S3.
|
||||
package ini_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/internal/ini"
|
||||
)
|
||||
|
||||
// TestFuzz is used to test for crashes and not validity of the input
|
||||
func TestFuzz(t *testing.T) {
|
||||
paths, err := filepath.Glob("testdata/fuzz/*")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, but received %v", err)
|
||||
}
|
||||
|
||||
if paths == nil {
|
||||
t.Errorf("expected fuzz files, but received none")
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
ini.OpenFile(path)
|
||||
})
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
)
|
||||
|
||||
// OpenFile takes a path to a given file, and will open and parse
|
||||
// that file.
|
||||
func OpenFile(path string) (Sections, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return Sections{}, awserr.New(ErrCodeUnableToReadFile, "unable to open file", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return Parse(f)
|
||||
}
|
||||
|
||||
// Parse will parse the given file using the shared config
|
||||
// visitor.
|
||||
func Parse(f io.Reader) (Sections, error) {
|
||||
tree, err := ParseAST(f)
|
||||
if err != nil {
|
||||
return Sections{}, err
|
||||
}
|
||||
|
||||
v := NewDefaultVisitor()
|
||||
if err = Walk(tree, v); err != nil {
|
||||
return Sections{}, err
|
||||
}
|
||||
|
||||
return v.Sections, nil
|
||||
}
|
||||
|
||||
// ParseBytes will parse the given bytes and return the parsed sections.
|
||||
func ParseBytes(b []byte) (Sections, error) {
|
||||
tree, err := ParseASTBytes(b)
|
||||
if err != nil {
|
||||
return Sections{}, err
|
||||
}
|
||||
|
||||
v := NewDefaultVisitor()
|
||||
if err = Walk(tree, v); err != nil {
|
||||
return Sections{}, err
|
||||
}
|
||||
|
||||
return v.Sections, nil
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrCodeUnableToReadFile is used when a file is failed to be
|
||||
// opened or read from.
|
||||
ErrCodeUnableToReadFile = "FailedRead"
|
||||
)
|
||||
|
||||
// TokenType represents the various different tokens types
|
||||
type TokenType int
|
||||
|
||||
func (t TokenType) String() string {
|
||||
switch t {
|
||||
case TokenNone:
|
||||
return "none"
|
||||
case TokenLit:
|
||||
return "literal"
|
||||
case TokenSep:
|
||||
return "sep"
|
||||
case TokenOp:
|
||||
return "op"
|
||||
case TokenWS:
|
||||
return "ws"
|
||||
case TokenNL:
|
||||
return "newline"
|
||||
case TokenComment:
|
||||
return "comment"
|
||||
case TokenComma:
|
||||
return "comma"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// TokenType enums
|
||||
const (
|
||||
TokenNone = TokenType(iota)
|
||||
TokenLit
|
||||
TokenSep
|
||||
TokenComma
|
||||
TokenOp
|
||||
TokenWS
|
||||
TokenNL
|
||||
TokenComment
|
||||
)
|
||||
|
||||
type iniLexer struct{}
|
||||
|
||||
// Tokenize will return a list of tokens during lexical analysis of the
|
||||
// io.Reader.
|
||||
func (l *iniLexer) Tokenize(r io.Reader) ([]Token, error) {
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, awserr.New(ErrCodeUnableToReadFile, "unable to read file", err)
|
||||
}
|
||||
|
||||
return l.tokenize(b)
|
||||
}
|
||||
|
||||
func (l *iniLexer) tokenize(b []byte) ([]Token, error) {
|
||||
runes := bytes.Runes(b)
|
||||
var err error
|
||||
n := 0
|
||||
tokenAmount := countTokens(runes)
|
||||
tokens := make([]Token, tokenAmount)
|
||||
count := 0
|
||||
|
||||
for len(runes) > 0 && count < tokenAmount {
|
||||
switch {
|
||||
case isWhitespace(runes[0]):
|
||||
tokens[count], n, err = newWSToken(runes)
|
||||
case isComma(runes[0]):
|
||||
tokens[count], n = newCommaToken(), 1
|
||||
case isComment(runes):
|
||||
tokens[count], n, err = newCommentToken(runes)
|
||||
case isNewline(runes):
|
||||
tokens[count], n, err = newNewlineToken(runes)
|
||||
case isSep(runes):
|
||||
tokens[count], n, err = newSepToken(runes)
|
||||
case isOp(runes):
|
||||
tokens[count], n, err = newOpToken(runes)
|
||||
default:
|
||||
tokens[count], n, err = newLitToken(runes)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
runes = runes[n:]
|
||||
}
|
||||
|
||||
return tokens[:count], nil
|
||||
}
|
||||
|
||||
func countTokens(runes []rune) int {
|
||||
count, n := 0, 0
|
||||
var err error
|
||||
|
||||
for len(runes) > 0 {
|
||||
switch {
|
||||
case isWhitespace(runes[0]):
|
||||
_, n, err = newWSToken(runes)
|
||||
case isComma(runes[0]):
|
||||
_, n = newCommaToken(), 1
|
||||
case isComment(runes):
|
||||
_, n, err = newCommentToken(runes)
|
||||
case isNewline(runes):
|
||||
_, n, err = newNewlineToken(runes)
|
||||
case isSep(runes):
|
||||
_, n, err = newSepToken(runes)
|
||||
case isOp(runes):
|
||||
_, n, err = newOpToken(runes)
|
||||
default:
|
||||
_, n, err = newLitToken(runes)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
count++
|
||||
runes = runes[n:]
|
||||
}
|
||||
|
||||
return count + 1
|
||||
}
|
||||
|
||||
// Token indicates a metadata about a given value.
|
||||
type Token struct {
|
||||
t TokenType
|
||||
ValueType ValueType
|
||||
base int
|
||||
raw []rune
|
||||
}
|
||||
|
||||
var emptyValue = Value{}
|
||||
|
||||
func newToken(t TokenType, raw []rune, v ValueType) Token {
|
||||
return Token{
|
||||
t: t,
|
||||
raw: raw,
|
||||
ValueType: v,
|
||||
}
|
||||
}
|
||||
|
||||
// Raw return the raw runes that were consumed
|
||||
func (tok Token) Raw() []rune {
|
||||
return tok.raw
|
||||
}
|
||||
|
||||
// Type returns the token type
|
||||
func (tok Token) Type() TokenType {
|
||||
return tok.t
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTokenize(t *testing.T) {
|
||||
numberToken := newToken(TokenLit, []rune("123"), IntegerType)
|
||||
numberToken.base = 10
|
||||
cases := []struct {
|
||||
r io.Reader
|
||||
expectedTokens []Token
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
r: bytes.NewBuffer([]byte(`x = 123`)),
|
||||
expectedTokens: []Token{
|
||||
newToken(TokenLit, []rune("x"), StringType),
|
||||
newToken(TokenWS, []rune(" "), NoneType),
|
||||
newToken(TokenOp, []rune("="), NoneType),
|
||||
newToken(TokenWS, []rune(" "), NoneType),
|
||||
numberToken,
|
||||
},
|
||||
},
|
||||
{
|
||||
r: bytes.NewBuffer([]byte(`[ foo ]`)),
|
||||
expectedTokens: []Token{
|
||||
newToken(TokenSep, []rune("["), NoneType),
|
||||
newToken(TokenWS, []rune(" "), NoneType),
|
||||
newToken(TokenLit, []rune("foo"), StringType),
|
||||
newToken(TokenWS, []rune(" "), NoneType),
|
||||
newToken(TokenSep, []rune("]"), NoneType),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
lex := iniLexer{}
|
||||
tokens, err := lex.Tokenize(c.r)
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("expected %t, but received %t", e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedTokens, tokens; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("expected %v, but received %v", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// State enums for the parse table
|
||||
const (
|
||||
InvalidState = iota
|
||||
// stmt -> value stmt'
|
||||
StatementState
|
||||
// stmt' -> MarkComplete | op stmt
|
||||
StatementPrimeState
|
||||
// value -> number | string | boolean | quoted_string
|
||||
ValueState
|
||||
// section -> [ section'
|
||||
OpenScopeState
|
||||
// section' -> value section_close
|
||||
SectionState
|
||||
// section_close -> ]
|
||||
CloseScopeState
|
||||
// SkipState will skip (NL WS)+
|
||||
SkipState
|
||||
// SkipTokenState will skip any token and push the previous
|
||||
// state onto the stack.
|
||||
SkipTokenState
|
||||
// comment -> # comment' | ; comment'
|
||||
// comment' -> MarkComplete | value
|
||||
CommentState
|
||||
// MarkComplete state will complete statements and move that
|
||||
// to the completed AST list
|
||||
MarkCompleteState
|
||||
// TerminalState signifies that the tokens have been fully parsed
|
||||
TerminalState
|
||||
)
|
||||
|
||||
// parseTable is a state machine to dictate the grammar above.
|
||||
var parseTable = map[ASTKind]map[TokenType]int{
|
||||
ASTKindStart: map[TokenType]int{
|
||||
TokenLit: StatementState,
|
||||
TokenSep: OpenScopeState,
|
||||
TokenWS: SkipTokenState,
|
||||
TokenNL: SkipTokenState,
|
||||
TokenComment: CommentState,
|
||||
TokenNone: TerminalState,
|
||||
},
|
||||
ASTKindCommentStatement: map[TokenType]int{
|
||||
TokenLit: StatementState,
|
||||
TokenSep: OpenScopeState,
|
||||
TokenWS: SkipTokenState,
|
||||
TokenNL: SkipTokenState,
|
||||
TokenComment: CommentState,
|
||||
TokenNone: MarkCompleteState,
|
||||
},
|
||||
ASTKindExpr: map[TokenType]int{
|
||||
TokenOp: StatementPrimeState,
|
||||
TokenLit: ValueState,
|
||||
TokenSep: OpenScopeState,
|
||||
TokenWS: ValueState,
|
||||
TokenNL: SkipState,
|
||||
TokenComment: CommentState,
|
||||
TokenNone: MarkCompleteState,
|
||||
},
|
||||
ASTKindEqualExpr: map[TokenType]int{
|
||||
TokenLit: ValueState,
|
||||
TokenWS: SkipTokenState,
|
||||
TokenNL: SkipState,
|
||||
},
|
||||
ASTKindStatement: map[TokenType]int{
|
||||
TokenLit: SectionState,
|
||||
TokenSep: CloseScopeState,
|
||||
TokenWS: SkipTokenState,
|
||||
TokenNL: SkipTokenState,
|
||||
TokenComment: CommentState,
|
||||
TokenNone: MarkCompleteState,
|
||||
},
|
||||
ASTKindExprStatement: map[TokenType]int{
|
||||
TokenLit: ValueState,
|
||||
TokenSep: OpenScopeState,
|
||||
TokenOp: ValueState,
|
||||
TokenWS: ValueState,
|
||||
TokenNL: MarkCompleteState,
|
||||
TokenComment: CommentState,
|
||||
TokenNone: TerminalState,
|
||||
TokenComma: SkipState,
|
||||
},
|
||||
ASTKindSectionStatement: map[TokenType]int{
|
||||
TokenLit: SectionState,
|
||||
TokenOp: SectionState,
|
||||
TokenSep: CloseScopeState,
|
||||
TokenWS: SectionState,
|
||||
TokenNL: SkipTokenState,
|
||||
},
|
||||
ASTKindCompletedSectionStatement: map[TokenType]int{
|
||||
TokenWS: SkipTokenState,
|
||||
TokenNL: SkipTokenState,
|
||||
TokenLit: StatementState,
|
||||
TokenSep: OpenScopeState,
|
||||
TokenComment: CommentState,
|
||||
TokenNone: MarkCompleteState,
|
||||
},
|
||||
ASTKindSkipStatement: map[TokenType]int{
|
||||
TokenLit: StatementState,
|
||||
TokenSep: OpenScopeState,
|
||||
TokenWS: SkipTokenState,
|
||||
TokenNL: SkipTokenState,
|
||||
TokenComment: CommentState,
|
||||
TokenNone: TerminalState,
|
||||
},
|
||||
}
|
||||
|
||||
// ParseAST will parse input from an io.Reader using
|
||||
// an LL(1) parser.
|
||||
func ParseAST(r io.Reader) ([]AST, error) {
|
||||
lexer := iniLexer{}
|
||||
tokens, err := lexer.Tokenize(r)
|
||||
if err != nil {
|
||||
return []AST{}, err
|
||||
}
|
||||
|
||||
return parse(tokens)
|
||||
}
|
||||
|
||||
// ParseASTBytes will parse input from a byte slice using
|
||||
// an LL(1) parser.
|
||||
func ParseASTBytes(b []byte) ([]AST, error) {
|
||||
lexer := iniLexer{}
|
||||
tokens, err := lexer.tokenize(b)
|
||||
if err != nil {
|
||||
return []AST{}, err
|
||||
}
|
||||
|
||||
return parse(tokens)
|
||||
}
|
||||
|
||||
func parse(tokens []Token) ([]AST, error) {
|
||||
start := Start
|
||||
stack := newParseStack(3, len(tokens))
|
||||
|
||||
stack.Push(start)
|
||||
s := newSkipper()
|
||||
|
||||
loop:
|
||||
for stack.Len() > 0 {
|
||||
k := stack.Pop()
|
||||
|
||||
var tok Token
|
||||
if len(tokens) == 0 {
|
||||
// this occurs when all the tokens have been processed
|
||||
// but reduction of what's left on the stack needs to
|
||||
// occur.
|
||||
tok = emptyToken
|
||||
} else {
|
||||
tok = tokens[0]
|
||||
}
|
||||
|
||||
step := parseTable[k.Kind][tok.Type()]
|
||||
if s.ShouldSkip(tok) {
|
||||
// being in a skip state with no tokens will break out of
|
||||
// the parse loop since there is nothing left to process.
|
||||
if len(tokens) == 0 {
|
||||
break loop
|
||||
}
|
||||
|
||||
step = SkipTokenState
|
||||
}
|
||||
|
||||
switch step {
|
||||
case TerminalState:
|
||||
// Finished parsing. Push what should be the last
|
||||
// statement to the stack. If there is anything left
|
||||
// on the stack, an error in parsing has occurred.
|
||||
if k.Kind != ASTKindStart {
|
||||
stack.MarkComplete(k)
|
||||
}
|
||||
break loop
|
||||
case SkipTokenState:
|
||||
// When skipping a token, the previous state was popped off the stack.
|
||||
// To maintain the correct state, the previous state will be pushed
|
||||
// onto the stack.
|
||||
stack.Push(k)
|
||||
case StatementState:
|
||||
if k.Kind != ASTKindStart {
|
||||
stack.MarkComplete(k)
|
||||
}
|
||||
expr := newExpression(tok)
|
||||
stack.Push(expr)
|
||||
case StatementPrimeState:
|
||||
if tok.Type() != TokenOp {
|
||||
stack.MarkComplete(k)
|
||||
continue
|
||||
}
|
||||
|
||||
if k.Kind != ASTKindExpr {
|
||||
return nil, NewParseError(
|
||||
fmt.Sprintf("invalid expression: expected Expr type, but found %T type", k),
|
||||
)
|
||||
}
|
||||
|
||||
k = trimSpaces(k)
|
||||
expr := newEqualExpr(k, tok)
|
||||
stack.Push(expr)
|
||||
case ValueState:
|
||||
// ValueState requires the previous state to either be an equal expression
|
||||
// or an expression statement.
|
||||
//
|
||||
// This grammar occurs when the RHS is a number, word, or quoted string.
|
||||
// equal_expr -> lit op equal_expr'
|
||||
// equal_expr' -> number | string | quoted_string
|
||||
// quoted_string -> " quoted_string'
|
||||
// quoted_string' -> string quoted_string_end
|
||||
// quoted_string_end -> "
|
||||
//
|
||||
// otherwise
|
||||
// expr_stmt -> equal_expr (expr_stmt')*
|
||||
// expr_stmt' -> ws S | op S | MarkComplete
|
||||
// S -> equal_expr' expr_stmt'
|
||||
switch k.Kind {
|
||||
case ASTKindEqualExpr:
|
||||
// assiging a value to some key
|
||||
k.AppendChild(newExpression(tok))
|
||||
stack.Push(newExprStatement(k))
|
||||
case ASTKindExpr:
|
||||
k.Root.raw = append(k.Root.raw, tok.Raw()...)
|
||||
stack.Push(k)
|
||||
case ASTKindExprStatement:
|
||||
root := k.GetRoot()
|
||||
children := root.GetChildren()
|
||||
if len(children) == 0 {
|
||||
return nil, NewParseError(
|
||||
fmt.Sprintf("invalid expression: AST contains no children %s", k.Kind),
|
||||
)
|
||||
}
|
||||
|
||||
rhs := children[len(children)-1]
|
||||
|
||||
if rhs.Root.ValueType != QuotedStringType {
|
||||
rhs.Root.ValueType = StringType
|
||||
rhs.Root.raw = append(rhs.Root.raw, tok.Raw()...)
|
||||
|
||||
}
|
||||
|
||||
children[len(children)-1] = rhs
|
||||
k.SetChildren(children)
|
||||
|
||||
stack.Push(k)
|
||||
}
|
||||
case OpenScopeState:
|
||||
if !runeCompare(tok.Raw(), openBrace) {
|
||||
return nil, NewParseError("expected '['")
|
||||
}
|
||||
|
||||
stmt := newStatement()
|
||||
stack.Push(stmt)
|
||||
case CloseScopeState:
|
||||
if !runeCompare(tok.Raw(), closeBrace) {
|
||||
return nil, NewParseError("expected ']'")
|
||||
}
|
||||
|
||||
k = trimSpaces(k)
|
||||
stack.Push(newCompletedSectionStatement(k))
|
||||
case SectionState:
|
||||
var stmt AST
|
||||
|
||||
switch k.Kind {
|
||||
case ASTKindStatement:
|
||||
// If there are multiple literals inside of a scope declaration,
|
||||
// then the current token's raw value will be appended to the Name.
|
||||
//
|
||||
// This handles cases like [ profile default ]
|
||||
//
|
||||
// k will represent a SectionStatement with the children representing
|
||||
// the label of the section
|
||||
stmt = newSectionStatement(tok)
|
||||
case ASTKindSectionStatement:
|
||||
k.Root.raw = append(k.Root.raw, tok.Raw()...)
|
||||
stmt = k
|
||||
default:
|
||||
return nil, NewParseError(
|
||||
fmt.Sprintf("invalid statement: expected statement: %v", k.Kind),
|
||||
)
|
||||
}
|
||||
|
||||
stack.Push(stmt)
|
||||
case MarkCompleteState:
|
||||
if k.Kind != ASTKindStart {
|
||||
stack.MarkComplete(k)
|
||||
}
|
||||
|
||||
if stack.Len() == 0 {
|
||||
stack.Push(start)
|
||||
}
|
||||
case SkipState:
|
||||
stack.Push(newSkipStatement(k))
|
||||
s.Skip()
|
||||
case CommentState:
|
||||
if k.Kind == ASTKindStart {
|
||||
stack.Push(k)
|
||||
} else {
|
||||
stack.MarkComplete(k)
|
||||
}
|
||||
|
||||
stmt := newCommentStatement(tok)
|
||||
stack.Push(stmt)
|
||||
default:
|
||||
return nil, NewParseError(
|
||||
fmt.Sprintf("invalid state with ASTKind %v and TokenType %v",
|
||||
k, tok.Type()))
|
||||
}
|
||||
|
||||
if len(tokens) > 0 {
|
||||
tokens = tokens[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// this occurs when a statement has not been completed
|
||||
if stack.top > 1 {
|
||||
return nil, NewParseError(fmt.Sprintf("incomplete ini expression"))
|
||||
}
|
||||
|
||||
// returns a sublist which excludes the start symbol
|
||||
return stack.List(), nil
|
||||
}
|
||||
|
||||
// trimSpaces will trim spaces on the left and right hand side of
|
||||
// the literal.
|
||||
func trimSpaces(k AST) AST {
|
||||
// trim left hand side of spaces
|
||||
for i := 0; i < len(k.Root.raw); i++ {
|
||||
if !isWhitespace(k.Root.raw[i]) {
|
||||
break
|
||||
}
|
||||
|
||||
k.Root.raw = k.Root.raw[1:]
|
||||
i--
|
||||
}
|
||||
|
||||
// trim right hand side of spaces
|
||||
for i := len(k.Root.raw) - 1; i >= 0; i-- {
|
||||
if !isWhitespace(k.Root.raw[i]) {
|
||||
break
|
||||
}
|
||||
|
||||
k.Root.raw = k.Root.raw[:len(k.Root.raw)-1]
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
xID, _, _ := newLitToken([]rune("x = 1234"))
|
||||
s3ID, _, _ := newLitToken([]rune("s3 = 1234"))
|
||||
fooSlashes, _, _ := newLitToken([]rune("//foo"))
|
||||
|
||||
regionID, _, _ := newLitToken([]rune("region"))
|
||||
regionLit, _, _ := newLitToken([]rune(`"us-west-2"`))
|
||||
regionNoQuotesLit, _, _ := newLitToken([]rune("us-west-2"))
|
||||
|
||||
credentialID, _, _ := newLitToken([]rune("credential_source"))
|
||||
ec2MetadataLit, _, _ := newLitToken([]rune("Ec2InstanceMetadata"))
|
||||
|
||||
outputID, _, _ := newLitToken([]rune("output"))
|
||||
outputLit, _, _ := newLitToken([]rune("json"))
|
||||
|
||||
equalOp, _, _ := newOpToken([]rune("= 1234"))
|
||||
equalColonOp, _, _ := newOpToken([]rune(": 1234"))
|
||||
numLit, _, _ := newLitToken([]rune("1234"))
|
||||
defaultID, _, _ := newLitToken([]rune("default"))
|
||||
assumeID, _, _ := newLitToken([]rune("assumerole"))
|
||||
|
||||
defaultProfileStmt := newSectionStatement(defaultID)
|
||||
assumeProfileStmt := newSectionStatement(assumeID)
|
||||
|
||||
fooSlashesExpr := newExpression(fooSlashes)
|
||||
|
||||
xEQ1234 := newEqualExpr(newExpression(xID), equalOp)
|
||||
xEQ1234.AppendChild(newExpression(numLit))
|
||||
xEQColon1234 := newEqualExpr(newExpression(xID), equalColonOp)
|
||||
xEQColon1234.AppendChild(newExpression(numLit))
|
||||
|
||||
regionEQRegion := newEqualExpr(newExpression(regionID), equalOp)
|
||||
regionEQRegion.AppendChild(newExpression(regionLit))
|
||||
|
||||
noQuotesRegionEQRegion := newEqualExpr(newExpression(regionID), equalOp)
|
||||
noQuotesRegionEQRegion.AppendChild(newExpression(regionNoQuotesLit))
|
||||
|
||||
credEQExpr := newEqualExpr(newExpression(credentialID), equalOp)
|
||||
credEQExpr.AppendChild(newExpression(ec2MetadataLit))
|
||||
|
||||
outputEQExpr := newEqualExpr(newExpression(outputID), equalOp)
|
||||
outputEQExpr.AppendChild(newExpression(outputLit))
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
r io.Reader
|
||||
expectedStack []AST
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "semicolon comment",
|
||||
r: bytes.NewBuffer([]byte(`;foo`)),
|
||||
expectedStack: []AST{
|
||||
newCommentStatement(newToken(TokenComment, []rune(";foo"), NoneType)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "0==0",
|
||||
r: bytes.NewBuffer([]byte(`0==0`)),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "0=:0",
|
||||
r: bytes.NewBuffer([]byte(`0=:0`)),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "0:=0",
|
||||
r: bytes.NewBuffer([]byte(`0:=0`)),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "0::0",
|
||||
r: bytes.NewBuffer([]byte(`0::0`)),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "section with variable",
|
||||
r: bytes.NewBuffer([]byte(`[ default ]x`)),
|
||||
expectedStack: []AST{
|
||||
newCompletedSectionStatement(
|
||||
defaultProfileStmt,
|
||||
),
|
||||
newExpression(xID),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "# comment",
|
||||
r: bytes.NewBuffer([]byte(`# foo`)),
|
||||
expectedStack: []AST{
|
||||
newCommentStatement(newToken(TokenComment, []rune("# foo"), NoneType)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "// not a comment",
|
||||
r: bytes.NewBuffer([]byte(`//foo`)),
|
||||
expectedStack: []AST{
|
||||
fooSlashesExpr,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple comments",
|
||||
r: bytes.NewBuffer([]byte(`;foo
|
||||
# baz
|
||||
`)),
|
||||
expectedStack: []AST{
|
||||
newCommentStatement(newToken(TokenComment, []rune(";foo"), NoneType)),
|
||||
newCommentStatement(newToken(TokenComment, []rune("# baz"), NoneType)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comment followed by skip state",
|
||||
r: bytes.NewBuffer([]byte(`;foo
|
||||
//foo
|
||||
# baz
|
||||
`)),
|
||||
expectedStack: []AST{
|
||||
newCommentStatement(newToken(TokenComment, []rune(";foo"), NoneType)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assignment",
|
||||
r: bytes.NewBuffer([]byte(`x = 1234`)),
|
||||
expectedStack: []AST{
|
||||
newExprStatement(xEQ1234),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assignment spaceless",
|
||||
r: bytes.NewBuffer([]byte(`x=1234`)),
|
||||
expectedStack: []AST{
|
||||
newExprStatement(xEQ1234),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assignment :",
|
||||
r: bytes.NewBuffer([]byte(`x : 1234`)),
|
||||
expectedStack: []AST{
|
||||
newExprStatement(xEQColon1234),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "assignment : no spaces",
|
||||
r: bytes.NewBuffer([]byte(`x:1234`)),
|
||||
expectedStack: []AST{
|
||||
newExprStatement(xEQColon1234),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "section expression",
|
||||
r: bytes.NewBuffer([]byte(`[ default ]`)),
|
||||
expectedStack: []AST{
|
||||
newCompletedSectionStatement(
|
||||
defaultProfileStmt,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "section expression no spaces",
|
||||
r: bytes.NewBuffer([]byte(`[default]`)),
|
||||
expectedStack: []AST{
|
||||
newCompletedSectionStatement(
|
||||
defaultProfileStmt,
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "section statement",
|
||||
r: bytes.NewBuffer([]byte(`[default]
|
||||
region="us-west-2"`)),
|
||||
expectedStack: []AST{
|
||||
newCompletedSectionStatement(
|
||||
defaultProfileStmt,
|
||||
),
|
||||
newExprStatement(regionEQRegion),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex section statement",
|
||||
r: bytes.NewBuffer([]byte(`[default]
|
||||
region = us-west-2
|
||||
credential_source = Ec2InstanceMetadata
|
||||
output = json
|
||||
|
||||
[assumerole]
|
||||
output = json
|
||||
region = us-west-2
|
||||
`)),
|
||||
expectedStack: []AST{
|
||||
newCompletedSectionStatement(
|
||||
defaultProfileStmt,
|
||||
),
|
||||
newExprStatement(noQuotesRegionEQRegion),
|
||||
newExprStatement(credEQExpr),
|
||||
newExprStatement(outputEQExpr),
|
||||
newCompletedSectionStatement(
|
||||
assumeProfileStmt,
|
||||
),
|
||||
newExprStatement(outputEQExpr),
|
||||
newExprStatement(noQuotesRegionEQRegion),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex section statement with nested params",
|
||||
r: bytes.NewBuffer([]byte(`[default]
|
||||
s3 =
|
||||
foo=bar
|
||||
bar=baz
|
||||
region = us-west-2
|
||||
credential_source = Ec2InstanceMetadata
|
||||
output = json
|
||||
|
||||
[assumerole]
|
||||
output = json
|
||||
region = us-west-2
|
||||
`)),
|
||||
expectedStack: []AST{
|
||||
newCompletedSectionStatement(
|
||||
defaultProfileStmt,
|
||||
),
|
||||
newSkipStatement(newEqualExpr(newExpression(s3ID), equalOp)),
|
||||
newExprStatement(noQuotesRegionEQRegion),
|
||||
newExprStatement(credEQExpr),
|
||||
newExprStatement(outputEQExpr),
|
||||
newCompletedSectionStatement(
|
||||
assumeProfileStmt,
|
||||
),
|
||||
newExprStatement(outputEQExpr),
|
||||
newExprStatement(noQuotesRegionEQRegion),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex section statement",
|
||||
r: bytes.NewBuffer([]byte(`[default]
|
||||
region = us-west-2
|
||||
credential_source = Ec2InstanceMetadata
|
||||
s3 =
|
||||
foo=bar
|
||||
bar=baz
|
||||
output = json
|
||||
|
||||
[assumerole]
|
||||
output = json
|
||||
region = us-west-2
|
||||
`)),
|
||||
expectedStack: []AST{
|
||||
newCompletedSectionStatement(
|
||||
defaultProfileStmt,
|
||||
),
|
||||
newExprStatement(noQuotesRegionEQRegion),
|
||||
newExprStatement(credEQExpr),
|
||||
newSkipStatement(newEqualExpr(newExpression(s3ID), equalOp)),
|
||||
newExprStatement(outputEQExpr),
|
||||
newCompletedSectionStatement(
|
||||
assumeProfileStmt,
|
||||
),
|
||||
newExprStatement(outputEQExpr),
|
||||
newExprStatement(noQuotesRegionEQRegion),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
stack, err := ParseAST(c.r)
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("%d: expected %t, but received %t with error %v", i, e, a, err)
|
||||
}
|
||||
|
||||
if e, a := len(c.expectedStack), len(stack); e != a {
|
||||
t.Errorf("expected same length %d, but received %d", e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedStack, stack; !reflect.DeepEqual(e, a) {
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString("expected:\n")
|
||||
for j := 0; j < len(e); j++ {
|
||||
buf.WriteString(fmt.Sprintf("\t%d: %v\n", j, e[j]))
|
||||
}
|
||||
|
||||
buf.WriteString("\nreceived:\n")
|
||||
for j := 0; j < len(a); j++ {
|
||||
buf.WriteString(fmt.Sprintf("\t%d: %v\n", j, a[j]))
|
||||
}
|
||||
|
||||
t.Errorf("%s", buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
runesTrue = []rune("true")
|
||||
runesFalse = []rune("false")
|
||||
)
|
||||
|
||||
var literalValues = [][]rune{
|
||||
runesTrue,
|
||||
runesFalse,
|
||||
}
|
||||
|
||||
func isBoolValue(b []rune) bool {
|
||||
for _, lv := range literalValues {
|
||||
if isLitValue(lv, b) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isLitValue(want, have []rune) bool {
|
||||
if len(have) < len(want) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(want); i++ {
|
||||
if want[i] != have[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isNumberValue will return whether not the leading characters in
|
||||
// a byte slice is a number. A number is delimited by whitespace or
|
||||
// the newline token.
|
||||
//
|
||||
// A number is defined to be in a binary, octal, decimal (int | float), hex format,
|
||||
// or in scientific notation.
|
||||
func isNumberValue(b []rune) bool {
|
||||
negativeIndex := 0
|
||||
helper := numberHelper{}
|
||||
needDigit := false
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
negativeIndex++
|
||||
|
||||
switch b[i] {
|
||||
case '-':
|
||||
if helper.IsNegative() || negativeIndex != 1 {
|
||||
return false
|
||||
}
|
||||
helper.Determine(b[i])
|
||||
needDigit = true
|
||||
continue
|
||||
case 'e', 'E':
|
||||
if err := helper.Determine(b[i]); err != nil {
|
||||
return false
|
||||
}
|
||||
negativeIndex = 0
|
||||
needDigit = true
|
||||
continue
|
||||
case 'b':
|
||||
if helper.numberFormat == hex {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case 'o', 'x':
|
||||
needDigit = true
|
||||
if i == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
fallthrough
|
||||
case '.':
|
||||
if err := helper.Determine(b[i]); err != nil {
|
||||
return false
|
||||
}
|
||||
needDigit = true
|
||||
continue
|
||||
}
|
||||
|
||||
if i > 0 && (isNewline(b[i:]) || isWhitespace(b[i])) {
|
||||
return !needDigit
|
||||
}
|
||||
|
||||
if !helper.CorrectByte(b[i]) {
|
||||
return false
|
||||
}
|
||||
needDigit = false
|
||||
}
|
||||
|
||||
return !needDigit
|
||||
}
|
||||
|
||||
func isValid(b []rune) (bool, int, error) {
|
||||
if len(b) == 0 {
|
||||
// TODO: should probably return an error
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
return isValidRune(b[0]), 1, nil
|
||||
}
|
||||
|
||||
func isValidRune(r rune) bool {
|
||||
return r != ':' && r != '=' && r != '[' && r != ']' && r != ' ' && r != '\n'
|
||||
}
|
||||
|
||||
// ValueType is an enum that will signify what type
|
||||
// the Value is
|
||||
type ValueType int
|
||||
|
||||
func (v ValueType) String() string {
|
||||
switch v {
|
||||
case NoneType:
|
||||
return "NONE"
|
||||
case DecimalType:
|
||||
return "FLOAT"
|
||||
case IntegerType:
|
||||
return "INT"
|
||||
case StringType:
|
||||
return "STRING"
|
||||
case BoolType:
|
||||
return "BOOL"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ValueType enums
|
||||
const (
|
||||
NoneType = ValueType(iota)
|
||||
DecimalType
|
||||
IntegerType
|
||||
StringType
|
||||
QuotedStringType
|
||||
BoolType
|
||||
)
|
||||
|
||||
// Value is a union container
|
||||
type Value struct {
|
||||
Type ValueType
|
||||
raw []rune
|
||||
|
||||
integer int64
|
||||
decimal float64
|
||||
boolean bool
|
||||
str string
|
||||
}
|
||||
|
||||
func newValue(t ValueType, base int, raw []rune) (Value, error) {
|
||||
v := Value{
|
||||
Type: t,
|
||||
raw: raw,
|
||||
}
|
||||
var err error
|
||||
|
||||
switch t {
|
||||
case DecimalType:
|
||||
v.decimal, err = strconv.ParseFloat(string(raw), 64)
|
||||
case IntegerType:
|
||||
if base != 10 {
|
||||
raw = raw[2:]
|
||||
}
|
||||
|
||||
v.integer, err = strconv.ParseInt(string(raw), base, 64)
|
||||
case StringType:
|
||||
v.str = string(raw)
|
||||
case QuotedStringType:
|
||||
v.str = string(raw[1 : len(raw)-1])
|
||||
case BoolType:
|
||||
v.boolean = runeCompare(v.raw, runesTrue)
|
||||
}
|
||||
|
||||
// issue 2253
|
||||
//
|
||||
// if the value trying to be parsed is too large, then we will use
|
||||
// the 'StringType' and raw value instead.
|
||||
if nerr, ok := err.(*strconv.NumError); ok && nerr.Err == strconv.ErrRange {
|
||||
v.Type = StringType
|
||||
v.str = string(raw)
|
||||
err = nil
|
||||
}
|
||||
|
||||
return v, err
|
||||
}
|
||||
|
||||
// Append will append values and change the type to a string
|
||||
// type.
|
||||
func (v *Value) Append(tok Token) {
|
||||
r := tok.Raw()
|
||||
if v.Type != QuotedStringType {
|
||||
v.Type = StringType
|
||||
r = tok.raw[1 : len(tok.raw)-1]
|
||||
}
|
||||
if tok.Type() != TokenLit {
|
||||
v.raw = append(v.raw, tok.Raw()...)
|
||||
} else {
|
||||
v.raw = append(v.raw, r...)
|
||||
}
|
||||
}
|
||||
|
||||
func (v Value) String() string {
|
||||
switch v.Type {
|
||||
case DecimalType:
|
||||
return fmt.Sprintf("decimal: %f", v.decimal)
|
||||
case IntegerType:
|
||||
return fmt.Sprintf("integer: %d", v.integer)
|
||||
case StringType:
|
||||
return fmt.Sprintf("string: %s", string(v.raw))
|
||||
case QuotedStringType:
|
||||
return fmt.Sprintf("quoted string: %s", string(v.raw))
|
||||
case BoolType:
|
||||
return fmt.Sprintf("bool: %t", v.boolean)
|
||||
default:
|
||||
return "union not set"
|
||||
}
|
||||
}
|
||||
|
||||
func newLitToken(b []rune) (Token, int, error) {
|
||||
n := 0
|
||||
var err error
|
||||
|
||||
token := Token{}
|
||||
if b[0] == '"' {
|
||||
n, err = getStringValue(b)
|
||||
if err != nil {
|
||||
return token, n, err
|
||||
}
|
||||
|
||||
token = newToken(TokenLit, b[:n], QuotedStringType)
|
||||
} else if isNumberValue(b) {
|
||||
var base int
|
||||
base, n, err = getNumericalValue(b)
|
||||
if err != nil {
|
||||
return token, 0, err
|
||||
}
|
||||
|
||||
value := b[:n]
|
||||
vType := IntegerType
|
||||
if contains(value, '.') || hasExponent(value) {
|
||||
vType = DecimalType
|
||||
}
|
||||
token = newToken(TokenLit, value, vType)
|
||||
token.base = base
|
||||
} else if isBoolValue(b) {
|
||||
n, err = getBoolValue(b)
|
||||
|
||||
token = newToken(TokenLit, b[:n], BoolType)
|
||||
} else {
|
||||
n, err = getValue(b)
|
||||
token = newToken(TokenLit, b[:n], StringType)
|
||||
}
|
||||
|
||||
return token, n, err
|
||||
}
|
||||
|
||||
// IntValue returns an integer value
|
||||
func (v Value) IntValue() int64 {
|
||||
return v.integer
|
||||
}
|
||||
|
||||
// FloatValue returns a float value
|
||||
func (v Value) FloatValue() float64 {
|
||||
return v.decimal
|
||||
}
|
||||
|
||||
// BoolValue returns a bool value
|
||||
func (v Value) BoolValue() bool {
|
||||
return v.boolean
|
||||
}
|
||||
|
||||
func isTrimmable(r rune) bool {
|
||||
switch r {
|
||||
case '\n', ' ':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StringValue returns the string value
|
||||
func (v Value) StringValue() string {
|
||||
switch v.Type {
|
||||
case StringType:
|
||||
return strings.TrimFunc(string(v.raw), isTrimmable)
|
||||
case QuotedStringType:
|
||||
// preserve all characters in the quotes
|
||||
return string(removeEscapedCharacters(v.raw[1 : len(v.raw)-1]))
|
||||
default:
|
||||
return strings.TrimFunc(string(v.raw), isTrimmable)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(runes []rune, c rune) bool {
|
||||
for i := 0; i < len(runes); i++ {
|
||||
if runes[i] == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func runeCompare(v1 []rune, v2 []rune) bool {
|
||||
if len(v1) != len(v2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(v1); i++ {
|
||||
if v1[i] != v2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsNumberValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
b []rune
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
"integer",
|
||||
[]rune("123"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"negative integer",
|
||||
[]rune("-123"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"decimal",
|
||||
[]rune("123.456"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"small e exponent",
|
||||
[]rune("1e234"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"big E exponent",
|
||||
[]rune("1E234"),
|
||||
true,
|
||||
},
|
||||
{
|
||||
"error case exponent base 16",
|
||||
[]rune("1ea4"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"error case negative",
|
||||
[]rune("1-23"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"error case multiple negative",
|
||||
[]rune("-1-23"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"error case end negative",
|
||||
[]rune("123-"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"error case non-number",
|
||||
[]rune("a"),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"utf8 whitespace",
|
||||
[]rune("00"),
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
if e, a := c.expected, isNumberValue(c.b); e != a {
|
||||
t.Errorf("%d: expected %t, but received %t", i+1, e, a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test errors
|
||||
func TestNewLiteralToken(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
b []rune
|
||||
expectedRead int
|
||||
expectedToken Token
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "numbers",
|
||||
b: []rune("123"),
|
||||
expectedRead: 3,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("123"),
|
||||
IntegerType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "decimal",
|
||||
b: []rune("123.456"),
|
||||
expectedRead: 7,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("123.456"),
|
||||
DecimalType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "two numbers",
|
||||
b: []rune("123 456"),
|
||||
expectedRead: 3,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("123"),
|
||||
IntegerType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "number followed by alpha",
|
||||
b: []rune("123 abc"),
|
||||
expectedRead: 3,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("123"),
|
||||
IntegerType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "quoted string followed by number",
|
||||
b: []rune(`"Hello" 123`),
|
||||
expectedRead: 7,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("Hello"),
|
||||
QuotedStringType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "quoted string",
|
||||
b: []rune(`"Hello World"`),
|
||||
expectedRead: 13,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("Hello World"),
|
||||
QuotedStringType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "boolean true",
|
||||
b: []rune("true"),
|
||||
expectedRead: 4,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("true"),
|
||||
BoolType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "boolean false",
|
||||
b: []rune("false"),
|
||||
expectedRead: 5,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("false"),
|
||||
BoolType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "utf8 whitespace",
|
||||
b: []rune("00"),
|
||||
expectedRead: 1,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("0"),
|
||||
IntegerType,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "utf8 whitespace expr",
|
||||
b: []rune("0=00"),
|
||||
expectedRead: 1,
|
||||
expectedToken: newToken(TokenLit,
|
||||
[]rune("0"),
|
||||
StringType,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
tok, n, err := newLitToken(c.b)
|
||||
|
||||
if e, a := c.expectedToken.ValueType, tok.ValueType; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedRead, n; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package ini
|
||||
|
||||
func isNewline(b []rune) bool {
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if b[0] == '\n' {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(b) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return b[0] == '\r' && b[1] == '\n'
|
||||
}
|
||||
|
||||
func newNewlineToken(b []rune) (Token, int, error) {
|
||||
i := 1
|
||||
if b[0] == '\r' && isNewline(b[1:]) {
|
||||
i++
|
||||
}
|
||||
|
||||
if !isNewline([]rune(b[:i])) {
|
||||
return emptyToken, 0, NewParseError("invalid new line token")
|
||||
}
|
||||
|
||||
return newToken(TokenNL, b[:i], NoneType), i, nil
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
none = numberFormat(iota)
|
||||
binary
|
||||
octal
|
||||
decimal
|
||||
hex
|
||||
exponent
|
||||
)
|
||||
|
||||
type numberFormat int
|
||||
|
||||
// numberHelper is used to dictate what format a number is in
|
||||
// and what to do for negative values. Since -1e-4 is a valid
|
||||
// number, we cannot just simply check for duplicate negatives.
|
||||
type numberHelper struct {
|
||||
numberFormat numberFormat
|
||||
|
||||
negative bool
|
||||
negativeExponent bool
|
||||
}
|
||||
|
||||
func (b numberHelper) Exists() bool {
|
||||
return b.numberFormat != none
|
||||
}
|
||||
|
||||
func (b numberHelper) IsNegative() bool {
|
||||
return b.negative || b.negativeExponent
|
||||
}
|
||||
|
||||
func (b *numberHelper) Determine(c rune) error {
|
||||
if b.Exists() {
|
||||
return NewParseError(fmt.Sprintf("multiple number formats: 0%v", string(c)))
|
||||
}
|
||||
|
||||
switch c {
|
||||
case 'b':
|
||||
b.numberFormat = binary
|
||||
case 'o':
|
||||
b.numberFormat = octal
|
||||
case 'x':
|
||||
b.numberFormat = hex
|
||||
case 'e', 'E':
|
||||
b.numberFormat = exponent
|
||||
case '-':
|
||||
if b.numberFormat != exponent {
|
||||
b.negative = true
|
||||
} else {
|
||||
b.negativeExponent = true
|
||||
}
|
||||
case '.':
|
||||
b.numberFormat = decimal
|
||||
default:
|
||||
return NewParseError(fmt.Sprintf("invalid number character: %v", string(c)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b numberHelper) CorrectByte(c rune) bool {
|
||||
switch {
|
||||
case b.numberFormat == binary:
|
||||
if !isBinaryByte(c) {
|
||||
return false
|
||||
}
|
||||
case b.numberFormat == octal:
|
||||
if !isOctalByte(c) {
|
||||
return false
|
||||
}
|
||||
case b.numberFormat == hex:
|
||||
if !isHexByte(c) {
|
||||
return false
|
||||
}
|
||||
case b.numberFormat == decimal:
|
||||
if !isDigit(c) {
|
||||
return false
|
||||
}
|
||||
case b.numberFormat == exponent:
|
||||
if !isDigit(c) {
|
||||
return false
|
||||
}
|
||||
case b.negativeExponent:
|
||||
if !isDigit(c) {
|
||||
return false
|
||||
}
|
||||
case b.negative:
|
||||
if !isDigit(c) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
if !isDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (b numberHelper) Base() int {
|
||||
switch b.numberFormat {
|
||||
case binary:
|
||||
return 2
|
||||
case octal:
|
||||
return 8
|
||||
case hex:
|
||||
return 16
|
||||
default:
|
||||
return 10
|
||||
}
|
||||
}
|
||||
|
||||
func (b numberHelper) String() string {
|
||||
buf := bytes.Buffer{}
|
||||
i := 0
|
||||
|
||||
switch b.numberFormat {
|
||||
case binary:
|
||||
i++
|
||||
buf.WriteString(strconv.Itoa(i) + ": binary format\n")
|
||||
case octal:
|
||||
i++
|
||||
buf.WriteString(strconv.Itoa(i) + ": octal format\n")
|
||||
case hex:
|
||||
i++
|
||||
buf.WriteString(strconv.Itoa(i) + ": hex format\n")
|
||||
case exponent:
|
||||
i++
|
||||
buf.WriteString(strconv.Itoa(i) + ": exponent format\n")
|
||||
default:
|
||||
i++
|
||||
buf.WriteString(strconv.Itoa(i) + ": integer format\n")
|
||||
}
|
||||
|
||||
if b.negative {
|
||||
i++
|
||||
buf.WriteString(strconv.Itoa(i) + ": negative format\n")
|
||||
}
|
||||
|
||||
if b.negativeExponent {
|
||||
i++
|
||||
buf.WriteString(strconv.Itoa(i) + ": negative exponent format\n")
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNumberHelper(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
determineIndex int
|
||||
|
||||
expectedExists []bool
|
||||
expectedErrors []bool
|
||||
expectedCorrects []bool
|
||||
expectedNegative bool
|
||||
expectedBase int
|
||||
}{
|
||||
{
|
||||
b: []rune("-10"),
|
||||
determineIndex: 0,
|
||||
expectedExists: []bool{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
expectedErrors: []bool{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
expectedCorrects: []bool{
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedNegative: true,
|
||||
expectedBase: 10,
|
||||
},
|
||||
{
|
||||
b: []rune("0x10"),
|
||||
determineIndex: 1,
|
||||
expectedExists: []bool{
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedErrors: []bool{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
expectedCorrects: []bool{
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedNegative: false,
|
||||
expectedBase: 16,
|
||||
},
|
||||
{
|
||||
b: []rune("0b101"),
|
||||
determineIndex: 1,
|
||||
expectedExists: []bool{
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedErrors: []bool{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
expectedCorrects: []bool{
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedNegative: false,
|
||||
expectedBase: 2,
|
||||
},
|
||||
{
|
||||
b: []rune("0o271"),
|
||||
determineIndex: 1,
|
||||
expectedExists: []bool{
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedErrors: []bool{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
expectedCorrects: []bool{
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedNegative: false,
|
||||
expectedBase: 8,
|
||||
},
|
||||
{
|
||||
b: []rune("99"),
|
||||
determineIndex: -1,
|
||||
expectedExists: []bool{
|
||||
false,
|
||||
false,
|
||||
},
|
||||
expectedErrors: []bool{
|
||||
false,
|
||||
false,
|
||||
},
|
||||
expectedCorrects: []bool{
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedNegative: false,
|
||||
expectedBase: 10,
|
||||
},
|
||||
{
|
||||
b: []rune("0o2o71"),
|
||||
determineIndex: 1,
|
||||
expectedExists: []bool{
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedErrors: []bool{
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
},
|
||||
expectedCorrects: []bool{
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
expectedNegative: false,
|
||||
expectedBase: 8,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
helper := numberHelper{}
|
||||
|
||||
for i := 0; i < len(c.b); i++ {
|
||||
if e, a := c.expectedExists[i], helper.Exists(); e != a {
|
||||
t.Errorf("expected %t, but received %t", e, a)
|
||||
}
|
||||
|
||||
if i == c.determineIndex {
|
||||
if e, a := c.expectedErrors[i], helper.Determine(c.b[i]) != nil; e != a {
|
||||
t.Errorf("expected %t, but received %t", e, a)
|
||||
}
|
||||
} else {
|
||||
if e, a := c.expectedCorrects[i], helper.CorrectByte(c.b[i]); e != a {
|
||||
t.Errorf("expected %t, but received %t", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if e, a := c.expectedNegative, helper.IsNegative(); e != a {
|
||||
t.Errorf("expected %t, but received %t", e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedBase, helper.Base(); e != a {
|
||||
t.Errorf("expected %d, but received %d", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
equalOp = []rune("=")
|
||||
equalColonOp = []rune(":")
|
||||
)
|
||||
|
||||
func isOp(b []rune) bool {
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch b[0] {
|
||||
case '=':
|
||||
return true
|
||||
case ':':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func newOpToken(b []rune) (Token, int, error) {
|
||||
tok := Token{}
|
||||
|
||||
switch b[0] {
|
||||
case '=':
|
||||
tok = newToken(TokenOp, equalOp, NoneType)
|
||||
case ':':
|
||||
tok = newToken(TokenOp, equalColonOp, NoneType)
|
||||
default:
|
||||
return tok, 0, NewParseError(fmt.Sprintf("unexpected op type, %v", b[0]))
|
||||
}
|
||||
return tok, 1, nil
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsOp(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
b: []rune(``),
|
||||
},
|
||||
{
|
||||
b: []rune("123"),
|
||||
},
|
||||
{
|
||||
b: []rune(`"wee"`),
|
||||
},
|
||||
{
|
||||
b: []rune("="),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
b: []rune(":"),
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
if e, a := c.expected, isOp(c.b); e != a {
|
||||
t.Errorf("%d: expected %t, but received %t", i+0, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOp(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
expectedRead int
|
||||
expectedError bool
|
||||
expectedToken Token
|
||||
}{
|
||||
{
|
||||
b: []rune("="),
|
||||
expectedRead: 1,
|
||||
expectedToken: newToken(TokenOp, []rune("="), NoneType),
|
||||
},
|
||||
{
|
||||
b: []rune(":"),
|
||||
expectedRead: 1,
|
||||
expectedToken: newToken(TokenOp, []rune(":"), NoneType),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
tok, n, err := newOpToken(c.b)
|
||||
|
||||
if e, a := c.expectedToken, tok; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedRead, n; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package ini
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
// ErrCodeParseError is returned when a parsing error
|
||||
// has occurred.
|
||||
ErrCodeParseError = "INIParseError"
|
||||
)
|
||||
|
||||
// ParseError is an error which is returned during any part of
|
||||
// the parsing process.
|
||||
type ParseError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
// NewParseError will return a new ParseError where message
|
||||
// is the description of the error.
|
||||
func NewParseError(message string) *ParseError {
|
||||
return &ParseError{
|
||||
msg: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Code will return the ErrCodeParseError
|
||||
func (err *ParseError) Code() string {
|
||||
return ErrCodeParseError
|
||||
}
|
||||
|
||||
// Message returns the error's message
|
||||
func (err *ParseError) Message() string {
|
||||
return err.msg
|
||||
}
|
||||
|
||||
// OrigError return nothing since there will never be any
|
||||
// original error.
|
||||
func (err *ParseError) OrigError() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (err *ParseError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", err.Code(), err.Message())
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ParseStack is a stack that contains a container, the stack portion,
|
||||
// and the list which is the list of ASTs that have been successfully
|
||||
// parsed.
|
||||
type ParseStack struct {
|
||||
top int
|
||||
container []AST
|
||||
list []AST
|
||||
index int
|
||||
}
|
||||
|
||||
func newParseStack(sizeContainer, sizeList int) ParseStack {
|
||||
return ParseStack{
|
||||
container: make([]AST, sizeContainer),
|
||||
list: make([]AST, sizeList),
|
||||
}
|
||||
}
|
||||
|
||||
// Pop will return and truncate the last container element.
|
||||
func (s *ParseStack) Pop() AST {
|
||||
s.top--
|
||||
return s.container[s.top]
|
||||
}
|
||||
|
||||
// Push will add the new AST to the container
|
||||
func (s *ParseStack) Push(ast AST) {
|
||||
s.container[s.top] = ast
|
||||
s.top++
|
||||
}
|
||||
|
||||
// MarkComplete will append the AST to the list of completed statements
|
||||
func (s *ParseStack) MarkComplete(ast AST) {
|
||||
s.list[s.index] = ast
|
||||
s.index++
|
||||
}
|
||||
|
||||
// List will return the completed statements
|
||||
func (s ParseStack) List() []AST {
|
||||
return s.list[:s.index]
|
||||
}
|
||||
|
||||
// Len will return the length of the container
|
||||
func (s *ParseStack) Len() int {
|
||||
return s.top
|
||||
}
|
||||
|
||||
func (s ParseStack) String() string {
|
||||
buf := bytes.Buffer{}
|
||||
for i, node := range s.list {
|
||||
buf.WriteString(fmt.Sprintf("%d: %v\n", i+1, node))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newMockAST(v []rune) AST {
|
||||
return newASTWithRootToken(ASTKindNone, Token{raw: v})
|
||||
}
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
cases := []struct {
|
||||
asts []AST
|
||||
expected []AST
|
||||
}{
|
||||
{
|
||||
asts: []AST{
|
||||
newMockAST([]rune("0")),
|
||||
newMockAST([]rune("1")),
|
||||
newMockAST([]rune("2")),
|
||||
newMockAST([]rune("3")),
|
||||
newMockAST([]rune("4")),
|
||||
},
|
||||
expected: []AST{
|
||||
newMockAST([]rune("0")),
|
||||
newMockAST([]rune("1")),
|
||||
newMockAST([]rune("2")),
|
||||
newMockAST([]rune("3")),
|
||||
newMockAST([]rune("4")),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p := newParseStack(10, 10)
|
||||
for _, ast := range c.asts {
|
||||
p.Push(ast)
|
||||
p.MarkComplete(ast)
|
||||
}
|
||||
|
||||
if e, a := len(c.expected), p.Len(); e != a {
|
||||
t.Errorf("expected the same legnth with %d, but received %d", e, a)
|
||||
}
|
||||
for i := len(c.expected) - 1; i >= 0; i-- {
|
||||
if e, a := c.expected[i], p.Pop(); !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("stack element %d invalid: expected %v, but received %v", i, e, a)
|
||||
}
|
||||
}
|
||||
|
||||
if e, a := len(c.expected), p.index; e != a {
|
||||
t.Errorf("expected %d, but received %d", e, a)
|
||||
}
|
||||
|
||||
if e, a := c.asts, p.list[:p.index]; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("expected %v, but received %v", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
emptyRunes = []rune{}
|
||||
)
|
||||
|
||||
func isSep(b []rune) bool {
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch b[0] {
|
||||
case '[', ']':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
openBrace = []rune("[")
|
||||
closeBrace = []rune("]")
|
||||
)
|
||||
|
||||
func newSepToken(b []rune) (Token, int, error) {
|
||||
tok := Token{}
|
||||
|
||||
switch b[0] {
|
||||
case '[':
|
||||
tok = newToken(TokenSep, openBrace, NoneType)
|
||||
case ']':
|
||||
tok = newToken(TokenSep, closeBrace, NoneType)
|
||||
default:
|
||||
return tok, 0, NewParseError(fmt.Sprintf("unexpected sep type, %v", b[0]))
|
||||
}
|
||||
return tok, 1, nil
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsSep(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
b: []rune(``),
|
||||
},
|
||||
{
|
||||
b: []rune(`"wee"`),
|
||||
},
|
||||
{
|
||||
b: []rune("["),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
b: []rune("]"),
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
if e, a := c.expected, isSep(c.b); e != a {
|
||||
t.Errorf("%d: expected %t, but received %t", i+0, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSep(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
expectedRead int
|
||||
expectedError bool
|
||||
expectedToken Token
|
||||
}{
|
||||
{
|
||||
b: []rune("["),
|
||||
expectedRead: 1,
|
||||
expectedToken: newToken(TokenSep, []rune("["), NoneType),
|
||||
},
|
||||
{
|
||||
b: []rune("]"),
|
||||
expectedRead: 1,
|
||||
expectedToken: newToken(TokenSep, []rune("]"), NoneType),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
tok, n, err := newSepToken(c.b)
|
||||
|
||||
if e, a := c.expectedToken, tok; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedRead, n; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package ini
|
||||
|
||||
// skipper is used to skip certain blocks of an ini file.
|
||||
// Currently skipper is used to skip nested blocks of ini
|
||||
// files. See example below
|
||||
//
|
||||
// [ foo ]
|
||||
// nested = ; this section will be skipped
|
||||
// a=b
|
||||
// c=d
|
||||
// bar=baz ; this will be included
|
||||
type skipper struct {
|
||||
shouldSkip bool
|
||||
TokenSet bool
|
||||
prevTok Token
|
||||
}
|
||||
|
||||
func newSkipper() skipper {
|
||||
return skipper{
|
||||
prevTok: emptyToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *skipper) ShouldSkip(tok Token) bool {
|
||||
if s.shouldSkip &&
|
||||
s.prevTok.Type() == TokenNL &&
|
||||
tok.Type() != TokenWS {
|
||||
|
||||
s.Continue()
|
||||
return false
|
||||
}
|
||||
s.prevTok = tok
|
||||
|
||||
return s.shouldSkip
|
||||
}
|
||||
|
||||
func (s *skipper) Skip() {
|
||||
s.shouldSkip = true
|
||||
s.prevTok = emptyToken
|
||||
}
|
||||
|
||||
func (s *skipper) Continue() {
|
||||
s.shouldSkip = false
|
||||
s.prevTok = emptyToken
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSkipper(t *testing.T) {
|
||||
idTok, _, _ := newLitToken([]rune("id"))
|
||||
nlTok := newToken(TokenNL, []rune("\n"), NoneType)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
Fn func(s *skipper)
|
||||
param Token
|
||||
expected bool
|
||||
expectedShouldSkip bool
|
||||
expectedPrevTok Token
|
||||
}{
|
||||
{
|
||||
name: "empty case",
|
||||
Fn: func(s *skipper) {
|
||||
},
|
||||
param: emptyToken,
|
||||
expectedPrevTok: emptyToken,
|
||||
},
|
||||
{
|
||||
name: "skip case",
|
||||
Fn: func(s *skipper) {
|
||||
s.Skip()
|
||||
},
|
||||
param: idTok,
|
||||
expectedShouldSkip: true,
|
||||
expected: true,
|
||||
expectedPrevTok: emptyToken,
|
||||
},
|
||||
{
|
||||
name: "continue case",
|
||||
Fn: func(s *skipper) {
|
||||
s.Continue()
|
||||
},
|
||||
param: emptyToken,
|
||||
expectedPrevTok: emptyToken,
|
||||
},
|
||||
{
|
||||
name: "skip then continue case",
|
||||
Fn: func(s *skipper) {
|
||||
s.Skip()
|
||||
s.Continue()
|
||||
},
|
||||
param: emptyToken,
|
||||
expectedPrevTok: emptyToken,
|
||||
},
|
||||
{
|
||||
name: "do not skip case",
|
||||
Fn: func(s *skipper) {
|
||||
s.Skip()
|
||||
s.prevTok = nlTok
|
||||
},
|
||||
param: idTok,
|
||||
expectedShouldSkip: true,
|
||||
expectedPrevTok: nlTok,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
s := newSkipper()
|
||||
c.Fn(&s)
|
||||
|
||||
if e, a := c.expectedShouldSkip, s.shouldSkip; e != a {
|
||||
t.Errorf("%s: expected %t, but received %t", c.name, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedPrevTok, s.prevTok; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("%s: expected %v, but received %v", c.name, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expected, s.ShouldSkip(c.param); e != a {
|
||||
t.Errorf("%s: expected %t, but received %t", c.name, e, a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package ini
|
||||
|
||||
// Statement is an empty AST mostly used for transitioning states.
|
||||
func newStatement() AST {
|
||||
return newAST(ASTKindStatement, AST{})
|
||||
}
|
||||
|
||||
// SectionStatement represents a section AST
|
||||
func newSectionStatement(tok Token) AST {
|
||||
return newASTWithRootToken(ASTKindSectionStatement, tok)
|
||||
}
|
||||
|
||||
// ExprStatement represents a completed expression AST
|
||||
func newExprStatement(ast AST) AST {
|
||||
return newAST(ASTKindExprStatement, ast)
|
||||
}
|
||||
|
||||
// CommentStatement represents a comment in the ini definition.
|
||||
//
|
||||
// grammar:
|
||||
// comment -> #comment' | ;comment'
|
||||
// comment' -> epsilon | value
|
||||
func newCommentStatement(tok Token) AST {
|
||||
return newAST(ASTKindCommentStatement, newExpression(tok))
|
||||
}
|
||||
|
||||
// CompletedSectionStatement represents a completed section
|
||||
func newCompletedSectionStatement(ast AST) AST {
|
||||
return newAST(ASTKindCompletedSectionStatement, ast)
|
||||
}
|
||||
|
||||
// SkipStatement is used to skip whole statements
|
||||
func newSkipStatement(ast AST) AST {
|
||||
return newAST(ASTKindSkipStatement, ast)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
[[ foo ]
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
[ default
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
[ default #]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
[arn:aws:sts::1234:assumed-role/My-Role/session-name]
|
||||
region = "foo-region"
|
||||
arn = arn:aws:sts::1234:assumed-role/My-Role/session-name
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"arn:aws:sts::1234:assumed-role/My-Role/session-name": {
|
||||
"region": "foo-region",
|
||||
"arn": "arn:aws:sts::1234:assumed-role/My-Role/session-name"
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
[ foo ]
|
||||
bar = "one","two", "three"
|
||||
baz = 123
|
||||
qux =
|
||||
moo = 123,456
|
||||
cow = 1
|
||||
milk = 123,456
|
||||
zed = "zee"
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"foo": {
|
||||
"baz": 123,
|
||||
"zed": "zee"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
[ default ]
|
||||
binary=0b1001
|
||||
octal=0o107
|
||||
ten=12
|
||||
hex=0xAFB1
|
||||
hex2=0xafb1
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"default": {
|
||||
"binary": 9,
|
||||
"octal": 71,
|
||||
"ten": 12,
|
||||
"hex": 44977,
|
||||
"hex2": 44977
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# comment here
|
||||
[ default ]
|
||||
region = "foo-region" # another comment
|
||||
output = json # comment again
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"default": {
|
||||
"region": "foo-region",
|
||||
"output": "json"
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
[ default ]
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"default": {
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
[ default ]
|
||||
foo = "\"bar\""
|
||||
baz = "qux\n"
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"default": {
|
||||
"foo": "\"bar\"",
|
||||
"baz": "qux\n"
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
[ default ]
|
||||
exponent = 1e4
|
||||
exponent_2 = 1E-4
|
||||
exponent_should_be_string = 0x1ob
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"default": {
|
||||
"exponent": 10000,
|
||||
"exponent_2": 0.0001,
|
||||
"exponent_should_be_string": "0x1ob"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
foo=bar
|
||||
[default]
|
||||
foo=bar
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"default": {
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[default]
|
||||
large_number = 1234567890123456789012345678901234567890
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"default": {
|
||||
"large_number": "1234567890123456789012345678901234567890"
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
[foo-profile]
|
||||
aws_access_key_id = accesskey
|
||||
aws_secret_access_key = secret
|
||||
aws_session_token = token
|
||||
aws_security_token = sectoken
|
||||
x_principal_arn = arn:aws:sts::myarn
|
||||
x_security_token_expires = time
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"foo-profile": {
|
||||
"aws_access_key_id": "accesskey",
|
||||
"aws_secret_access_key": "secret",
|
||||
"aws_session_token": "token",
|
||||
"aws_security_token": "sectoken",
|
||||
"x_principal_arn": "arn:aws:sts::myarn",
|
||||
"x_security_token_expires": "time"
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[default]
|
||||
123 = 456.456
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"default": {
|
||||
"123": 456.456
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[ profile foo ]
|
||||
bar = baz
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"profile foo": {
|
||||
"bar": "baz"
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
[ default ]
|
||||
region = "foo-region"
|
||||
output = json
|
||||
|
||||
[ foo ]
|
||||
bar = baz
|
||||
[bar]
|
||||
baz=qux
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"default": {
|
||||
"region": "foo-region",
|
||||
"output": "json"
|
||||
},
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
},
|
||||
"bar": {
|
||||
"baz": "qux"
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
[ default ]
|
||||
region = "foo-region"
|
||||
output = json
|
||||
foo = bar = baz
|
||||
bar = baz qux
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"default": {
|
||||
"output": "json",
|
||||
"region": "foo-region",
|
||||
"foo": "bar = baz",
|
||||
"bar": "baz qux"
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
[ hyphen-profile-name ]
|
||||
aws region = "foo-region"
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"hyphen-profile-name": {
|
||||
"aws region": "foo-region"
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
[ ʃʉʍΡιξ ]
|
||||
ϰϪϧ = Ϯϴϖ
|
||||
ϝϧ = "ϟΞ΅"
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ʃʉʍΡιξ": {
|
||||
"ϰϪϧ": "Ϯϴϖ",
|
||||
"ϝϧ": "ϟΞ΅"
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTrimSpaces(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
node AST
|
||||
expectedNode AST
|
||||
}{
|
||||
{
|
||||
name: "simple case",
|
||||
node: AST{
|
||||
Root: Token{
|
||||
raw: []rune("foo"),
|
||||
},
|
||||
},
|
||||
expectedNode: AST{
|
||||
Root: Token{
|
||||
raw: []rune("foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "LHS case",
|
||||
node: AST{
|
||||
Root: Token{
|
||||
raw: []rune(" foo"),
|
||||
},
|
||||
},
|
||||
expectedNode: AST{
|
||||
Root: Token{
|
||||
raw: []rune("foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "RHS case",
|
||||
node: AST{
|
||||
Root: Token{
|
||||
raw: []rune("foo "),
|
||||
},
|
||||
},
|
||||
expectedNode: AST{
|
||||
Root: Token{
|
||||
raw: []rune("foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "both sides case",
|
||||
node: AST{
|
||||
Root: Token{
|
||||
raw: []rune(" foo "),
|
||||
},
|
||||
},
|
||||
expectedNode: AST{
|
||||
Root: Token{
|
||||
raw: []rune("foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
node := trimSpaces(c.node)
|
||||
|
||||
if e, a := c.expectedNode, node; !reflect.DeepEqual(e, a) {
|
||||
t.Errorf("%s: expected %v, but received %v", c.name, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// getStringValue will return a quoted string and the amount
|
||||
// of bytes read
|
||||
//
|
||||
// an error will be returned if the string is not properly formatted
|
||||
func getStringValue(b []rune) (int, error) {
|
||||
if b[0] != '"' {
|
||||
return 0, NewParseError("strings must start with '\"'")
|
||||
}
|
||||
|
||||
endQuote := false
|
||||
i := 1
|
||||
|
||||
for ; i < len(b) && !endQuote; i++ {
|
||||
if escaped := isEscaped(b[:i], b[i]); b[i] == '"' && !escaped {
|
||||
endQuote = true
|
||||
break
|
||||
} else if escaped {
|
||||
/*c, err := getEscapedByte(b[i])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
b[i-1] = c
|
||||
b = append(b[:i], b[i+1:]...)
|
||||
i--*/
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !endQuote {
|
||||
return 0, NewParseError("missing '\"' in string value")
|
||||
}
|
||||
|
||||
return i + 1, nil
|
||||
}
|
||||
|
||||
// getBoolValue will return a boolean and the amount
|
||||
// of bytes read
|
||||
//
|
||||
// an error will be returned if the boolean is not of a correct
|
||||
// value
|
||||
func getBoolValue(b []rune) (int, error) {
|
||||
if len(b) < 4 {
|
||||
return 0, NewParseError("invalid boolean value")
|
||||
}
|
||||
|
||||
n := 0
|
||||
for _, lv := range literalValues {
|
||||
if len(lv) > len(b) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isLitValue(lv, b) {
|
||||
n = len(lv)
|
||||
}
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return 0, NewParseError("invalid boolean value")
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// getNumericalValue will return a numerical string, the amount
|
||||
// of bytes read, and the base of the number
|
||||
//
|
||||
// an error will be returned if the number is not of a correct
|
||||
// value
|
||||
func getNumericalValue(b []rune) (int, int, error) {
|
||||
if !isDigit(b[0]) {
|
||||
return 0, 0, NewParseError("invalid digit value")
|
||||
}
|
||||
|
||||
i := 0
|
||||
helper := numberHelper{}
|
||||
|
||||
loop:
|
||||
for negativeIndex := 0; i < len(b); i++ {
|
||||
negativeIndex++
|
||||
|
||||
if !isDigit(b[i]) {
|
||||
switch b[i] {
|
||||
case '-':
|
||||
if helper.IsNegative() || negativeIndex != 1 {
|
||||
return 0, 0, NewParseError("parse error '-'")
|
||||
}
|
||||
|
||||
n := getNegativeNumber(b[i:])
|
||||
i += (n - 1)
|
||||
helper.Determine(b[i])
|
||||
continue
|
||||
case '.':
|
||||
if err := helper.Determine(b[i]); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
case 'e', 'E':
|
||||
if err := helper.Determine(b[i]); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
negativeIndex = 0
|
||||
case 'b':
|
||||
if helper.numberFormat == hex {
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
case 'o', 'x':
|
||||
if i == 0 && b[i] != '0' {
|
||||
return 0, 0, NewParseError("incorrect base format, expected leading '0'")
|
||||
}
|
||||
|
||||
if i != 1 {
|
||||
return 0, 0, NewParseError(fmt.Sprintf("incorrect base format found %s at %d index", string(b[i]), i))
|
||||
}
|
||||
|
||||
if err := helper.Determine(b[i]); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
default:
|
||||
if isWhitespace(b[i]) {
|
||||
break loop
|
||||
}
|
||||
|
||||
if isNewline(b[i:]) {
|
||||
break loop
|
||||
}
|
||||
|
||||
if !(helper.numberFormat == hex && isHexByte(b[i])) {
|
||||
if i+2 < len(b) && !isNewline(b[i:i+2]) {
|
||||
return 0, 0, NewParseError("invalid numerical character")
|
||||
} else if !isNewline([]rune{b[i]}) {
|
||||
return 0, 0, NewParseError("invalid numerical character")
|
||||
}
|
||||
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return helper.Base(), i, nil
|
||||
}
|
||||
|
||||
// isDigit will return whether or not something is an integer
|
||||
func isDigit(b rune) bool {
|
||||
return b >= '0' && b <= '9'
|
||||
}
|
||||
|
||||
func hasExponent(v []rune) bool {
|
||||
return contains(v, 'e') || contains(v, 'E')
|
||||
}
|
||||
|
||||
func isBinaryByte(b rune) bool {
|
||||
switch b {
|
||||
case '0', '1':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isOctalByte(b rune) bool {
|
||||
switch b {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isHexByte(b rune) bool {
|
||||
if isDigit(b) {
|
||||
return true
|
||||
}
|
||||
return (b >= 'A' && b <= 'F') ||
|
||||
(b >= 'a' && b <= 'f')
|
||||
}
|
||||
|
||||
func getValue(b []rune) (int, error) {
|
||||
i := 0
|
||||
|
||||
for i < len(b) {
|
||||
if isNewline(b[i:]) {
|
||||
break
|
||||
}
|
||||
|
||||
if isOp(b[i:]) {
|
||||
break
|
||||
}
|
||||
|
||||
valid, n, err := isValid(b[i:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
break
|
||||
}
|
||||
|
||||
i += n
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// getNegativeNumber will return a negative number from a
|
||||
// byte slice. This will iterate through all characters until
|
||||
// a non-digit has been found.
|
||||
func getNegativeNumber(b []rune) int {
|
||||
if b[0] != '-' {
|
||||
return 0
|
||||
}
|
||||
|
||||
i := 1
|
||||
for ; i < len(b); i++ {
|
||||
if !isDigit(b[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// isEscaped will return whether or not the character is an escaped
|
||||
// character.
|
||||
func isEscaped(value []rune, b rune) bool {
|
||||
if len(value) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch b {
|
||||
case '\'': // single quote
|
||||
case '"': // quote
|
||||
case 'n': // newline
|
||||
case 't': // tab
|
||||
case '\\': // backslash
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return value[len(value)-1] == '\\'
|
||||
}
|
||||
|
||||
func getEscapedByte(b rune) (rune, error) {
|
||||
switch b {
|
||||
case '\'': // single quote
|
||||
return '\'', nil
|
||||
case '"': // quote
|
||||
return '"', nil
|
||||
case 'n': // newline
|
||||
return '\n', nil
|
||||
case 't': // table
|
||||
return '\t', nil
|
||||
case '\\': // backslash
|
||||
return '\\', nil
|
||||
default:
|
||||
return b, NewParseError(fmt.Sprintf("invalid escaped character %c", b))
|
||||
}
|
||||
}
|
||||
|
||||
func removeEscapedCharacters(b []rune) []rune {
|
||||
for i := 0; i < len(b); i++ {
|
||||
if isEscaped(b[:i], b[i]) {
|
||||
c, err := getEscapedByte(b[i])
|
||||
if err != nil {
|
||||
return b
|
||||
}
|
||||
|
||||
b[i-1] = c
|
||||
b = append(b[:i], b[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStringValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
expectedRead int
|
||||
expectedError bool
|
||||
expectedValue string
|
||||
}{
|
||||
{
|
||||
b: []rune(`"foo"`),
|
||||
expectedRead: 5,
|
||||
expectedValue: `"foo"`,
|
||||
},
|
||||
{
|
||||
b: []rune(`"123 !$_ 456 abc"`),
|
||||
expectedRead: 17,
|
||||
expectedValue: `"123 !$_ 456 abc"`,
|
||||
},
|
||||
{
|
||||
b: []rune("foo"),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
b: []rune(` "foo"`),
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
n, err := getStringValue(c.b)
|
||||
|
||||
if e, a := c.expectedValue, string(c.b[:n]); e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedRead, n; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoolValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
expectedRead int
|
||||
expectedError bool
|
||||
expectedValue string
|
||||
}{
|
||||
{
|
||||
b: []rune("true"),
|
||||
expectedRead: 4,
|
||||
expectedValue: "true",
|
||||
},
|
||||
{
|
||||
b: []rune("false"),
|
||||
expectedRead: 5,
|
||||
expectedValue: "false",
|
||||
},
|
||||
{
|
||||
b: []rune(`"false"`),
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
n, err := getBoolValue(c.b)
|
||||
|
||||
if e, a := c.expectedValue, string(c.b[:n]); e != a {
|
||||
t.Errorf("expected %v, but received %v", e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedRead, n; e != a {
|
||||
t.Errorf("expected %v, but received %v", e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("expected %v, but received %v", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumericalValue(t *testing.T) {
|
||||
cases := []struct {
|
||||
b []rune
|
||||
expectedRead int
|
||||
expectedError bool
|
||||
expectedValue string
|
||||
expectedBase int
|
||||
}{
|
||||
{
|
||||
b: []rune("1.2"),
|
||||
expectedRead: 3,
|
||||
expectedValue: "1.2",
|
||||
expectedBase: 10,
|
||||
},
|
||||
{
|
||||
b: []rune("123"),
|
||||
expectedRead: 3,
|
||||
expectedValue: "123",
|
||||
expectedBase: 10,
|
||||
},
|
||||
{
|
||||
b: []rune("0x123A"),
|
||||
expectedRead: 6,
|
||||
expectedValue: "0x123A",
|
||||
expectedBase: 16,
|
||||
},
|
||||
{
|
||||
b: []rune("0b101"),
|
||||
expectedRead: 5,
|
||||
expectedValue: "0b101",
|
||||
expectedBase: 2,
|
||||
},
|
||||
{
|
||||
b: []rune("0o7"),
|
||||
expectedRead: 3,
|
||||
expectedValue: "0o7",
|
||||
expectedBase: 8,
|
||||
},
|
||||
{
|
||||
b: []rune(`"123"`),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
b: []rune("0xo123"),
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
b: []rune("123A"),
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
base, n, err := getNumericalValue(c.b)
|
||||
|
||||
if e, a := c.expectedValue, string(c.b[:n]); e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedRead, n; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedError, err != nil; e != a {
|
||||
t.Errorf("%d: expected %v, but received %v", i+1, e, a)
|
||||
}
|
||||
|
||||
if e, a := c.expectedBase, base; e != a {
|
||||
t.Errorf("%d: expected %d, but received %d", i+1, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Visitor is an interface used by walkers that will
|
||||
// traverse an array of ASTs.
|
||||
type Visitor interface {
|
||||
VisitExpr(AST) error
|
||||
VisitStatement(AST) error
|
||||
}
|
||||
|
||||
// DefaultVisitor is used to visit statements and expressions
|
||||
// and ensure that they are both of the correct format.
|
||||
// In addition, upon visiting this will build sections and populate
|
||||
// the Sections field which can be used to retrieve profile
|
||||
// configuration.
|
||||
type DefaultVisitor struct {
|
||||
scope string
|
||||
Sections Sections
|
||||
}
|
||||
|
||||
// NewDefaultVisitor return a DefaultVisitor
|
||||
func NewDefaultVisitor() *DefaultVisitor {
|
||||
return &DefaultVisitor{
|
||||
Sections: Sections{
|
||||
container: map[string]Section{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VisitExpr visits expressions...
|
||||
func (v *DefaultVisitor) VisitExpr(expr AST) error {
|
||||
t := v.Sections.container[v.scope]
|
||||
if t.values == nil {
|
||||
t.values = values{}
|
||||
}
|
||||
|
||||
switch expr.Kind {
|
||||
case ASTKindExprStatement:
|
||||
opExpr := expr.GetRoot()
|
||||
switch opExpr.Kind {
|
||||
case ASTKindEqualExpr:
|
||||
children := opExpr.GetChildren()
|
||||
if len(children) <= 1 {
|
||||
return NewParseError("unexpected token type")
|
||||
}
|
||||
|
||||
rhs := children[1]
|
||||
|
||||
if rhs.Root.Type() != TokenLit {
|
||||
return NewParseError("unexpected token type")
|
||||
}
|
||||
|
||||
key := EqualExprKey(opExpr)
|
||||
v, err := newValue(rhs.Root.ValueType, rhs.Root.base, rhs.Root.Raw())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.values[key] = v
|
||||
default:
|
||||
return NewParseError(fmt.Sprintf("unsupported expression %v", expr))
|
||||
}
|
||||
default:
|
||||
return NewParseError(fmt.Sprintf("unsupported expression %v", expr))
|
||||
}
|
||||
|
||||
v.Sections.container[v.scope] = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitStatement visits statements...
|
||||
func (v *DefaultVisitor) VisitStatement(stmt AST) error {
|
||||
switch stmt.Kind {
|
||||
case ASTKindCompletedSectionStatement:
|
||||
child := stmt.GetRoot()
|
||||
if child.Kind != ASTKindSectionStatement {
|
||||
return NewParseError(fmt.Sprintf("unsupported child statement: %T", child))
|
||||
}
|
||||
|
||||
name := string(child.Root.Raw())
|
||||
v.Sections.container[name] = Section{}
|
||||
v.scope = name
|
||||
default:
|
||||
return NewParseError(fmt.Sprintf("unsupported statement: %s", stmt.Kind))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sections is a map of Section structures that represent
|
||||
// a configuration.
|
||||
type Sections struct {
|
||||
container map[string]Section
|
||||
}
|
||||
|
||||
// GetSection will return section p. If section p does not exist,
|
||||
// false will be returned in the second parameter.
|
||||
func (t Sections) GetSection(p string) (Section, bool) {
|
||||
v, ok := t.container[p]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// values represents a map of union values.
|
||||
type values map[string]Value
|
||||
|
||||
// List will return a list of all sections that were successfully
|
||||
// parsed.
|
||||
func (t Sections) List() []string {
|
||||
keys := make([]string, len(t.container))
|
||||
i := 0
|
||||
for k := range t.container {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// Section contains a name and values. This represent
|
||||
// a sectioned entry in a configuration file.
|
||||
type Section struct {
|
||||
Name string
|
||||
values values
|
||||
}
|
||||
|
||||
// Has will return whether or not an entry exists in a given section
|
||||
func (t Section) Has(k string) bool {
|
||||
_, ok := t.values[k]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ValueType will returned what type the union is set to. If
|
||||
// k was not found, the NoneType will be returned.
|
||||
func (t Section) ValueType(k string) (ValueType, bool) {
|
||||
v, ok := t.values[k]
|
||||
return v.Type, ok
|
||||
}
|
||||
|
||||
// Bool returns a bool value at k
|
||||
func (t Section) Bool(k string) bool {
|
||||
return t.values[k].BoolValue()
|
||||
}
|
||||
|
||||
// Int returns an integer value at k
|
||||
func (t Section) Int(k string) int64 {
|
||||
return t.values[k].IntValue()
|
||||
}
|
||||
|
||||
// Float64 returns a float value at k
|
||||
func (t Section) Float64(k string) float64 {
|
||||
return t.values[k].FloatValue()
|
||||
}
|
||||
|
||||
// String returns the string value at k
|
||||
func (t Section) String(k string) string {
|
||||
_, ok := t.values[k]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return t.values[k].StringValue()
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package ini
|
||||
|
||||
// Walk will traverse the AST using the v, the Visitor.
|
||||
func Walk(tree []AST, v Visitor) error {
|
||||
for _, node := range tree {
|
||||
switch node.Kind {
|
||||
case ASTKindExpr,
|
||||
ASTKindExprStatement:
|
||||
|
||||
if err := v.VisitExpr(node); err != nil {
|
||||
return err
|
||||
}
|
||||
case ASTKindStatement,
|
||||
ASTKindCompletedSectionStatement,
|
||||
ASTKindNestedSectionStatement,
|
||||
ASTKindCompletedNestedSectionStatement:
|
||||
|
||||
if err := v.VisitStatement(node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// +build go1.7
|
||||
|
||||
package ini
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidDataFiles(t *testing.T) {
|
||||
const expectedFileSuffix = "_expected"
|
||||
filepath.Walk("./testdata/valid", func(path string, info os.FileInfo, err error) error {
|
||||
if strings.HasSuffix(path, expectedFileSuffix) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error, %v", path, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tree, err := ParseAST(f)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected parse error, %v", path, err)
|
||||
}
|
||||
|
||||
v := NewDefaultVisitor()
|
||||
err = Walk(tree, v)
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected walk error, %v", path, err)
|
||||
}
|
||||
|
||||
expectedPath := path + "_expected"
|
||||
e := map[string]interface{}{}
|
||||
|
||||
b, err := ioutil.ReadFile(expectedPath)
|
||||
if err != nil {
|
||||
// ignore files that do not have an expected file
|
||||
return nil
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &e)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error during deserialization, %v", err)
|
||||
}
|
||||
|
||||
for profile, tableIface := range e {
|
||||
p, ok := v.Sections.GetSection(profile)
|
||||
if !ok {
|
||||
t.Fatal("could not find profile " + profile)
|
||||
}
|
||||
|
||||
table := tableIface.(map[string]interface{})
|
||||
for k, v := range table {
|
||||
switch e := v.(type) {
|
||||
case string:
|
||||
a := p.String(k)
|
||||
if e != a {
|
||||
t.Errorf("%s: expected %v, but received %v", path, e, a)
|
||||
}
|
||||
case int:
|
||||
a := p.Int(k)
|
||||
if int64(e) != a {
|
||||
t.Errorf("%s: expected %v, but received %v", path, e, a)
|
||||
}
|
||||
case float64:
|
||||
v := p.values[k]
|
||||
if v.Type == IntegerType {
|
||||
a := p.Int(k)
|
||||
if int64(e) != a {
|
||||
t.Errorf("%s: expected %v, but received %v", path, e, a)
|
||||
}
|
||||
} else {
|
||||
a := p.Float64(k)
|
||||
if e != a {
|
||||
t.Errorf("%s: expected %v, but received %v", path, e, a)
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Errorf("unexpected type: %T", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvalidDataFiles(t *testing.T) {
|
||||
cases := []struct {
|
||||
path string
|
||||
expectedParseError bool
|
||||
expectedWalkError bool
|
||||
}{
|
||||
{
|
||||
path: "./testdata/invalid/bad_syntax_1",
|
||||
expectedParseError: true,
|
||||
},
|
||||
{
|
||||
path: "./testdata/invalid/incomplete_section_profile",
|
||||
expectedParseError: true,
|
||||
},
|
||||
{
|
||||
path: "./testdata/invalid/syntax_error_comment",
|
||||
expectedParseError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
t.Run(c.path, func(t *testing.T) {
|
||||
f, err := os.Open(c.path)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error, %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tree, err := ParseAST(f)
|
||||
if err != nil && !c.expectedParseError {
|
||||
t.Errorf("%d: unexpected error, %v", i+1, err)
|
||||
} else if err == nil && c.expectedParseError {
|
||||
t.Errorf("%d: expected error, but received none", i+1)
|
||||
}
|
||||
|
||||
if c.expectedParseError {
|
||||
return
|
||||
}
|
||||
|
||||
v := NewDefaultVisitor()
|
||||
err = Walk(tree, v)
|
||||
if err == nil && c.expectedWalkError {
|
||||
t.Errorf("%d: expected error, but received none", i+1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package ini
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// isWhitespace will return whether or not the character is
|
||||
// a whitespace character.
|
||||
//
|
||||
// Whitespace is defined as a space or tab.
|
||||
func isWhitespace(c rune) bool {
|
||||
return unicode.IsSpace(c) && c != '\n' && c != '\r'
|
||||
}
|
||||
|
||||
func newWSToken(b []rune) (Token, int, error) {
|
||||
i := 0
|
||||
for ; i < len(b); i++ {
|
||||
if !isWhitespace(b[i]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return newToken(TokenWS, b[:i], NoneType), i, nil
|
||||
}
|
||||
Reference in New Issue
Block a user