From a410399d2c85888843bf75f4691886acdecd9d69 Mon Sep 17 00:00:00 2001 From: Kazuyoshi Kato Date: Thu, 19 Oct 2017 07:56:30 -0700 Subject: [PATCH] Support single quoted keys (#193) Fixes #61 --- keysparsing.go | 109 +++++++++++++++++++++++++++++++++++--------- keysparsing_test.go | 16 ++++++- 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/keysparsing.go b/keysparsing.go index d62ca5f..9707c68 100644 --- a/keysparsing.go +++ b/keysparsing.go @@ -6,15 +6,37 @@ import ( "bytes" "errors" "fmt" + "strconv" "unicode" ) +var escapeSequenceMap = map[rune]rune{ + 'b': '\b', + 't': '\t', + 'n': '\n', + 'f': '\f', + 'r': '\r', + '"': '"', + '\\': '\\', +} + +type parseKeyState int + +const ( + BARE parseKeyState = iota + BASIC + LITERAL + ESC + UNICODE_4 + UNICODE_8 +) + func parseKey(key string) ([]string, error) { groups := []string{} var buffer bytes.Buffer - inQuotes := false + var hex bytes.Buffer + state := BARE wasInQuotes := false - escapeNext := false ignoreSpace := true expectDot := false @@ -25,25 +47,67 @@ func parseKey(key string) ([]string, error) { } ignoreSpace = false } - if escapeNext { - buffer.WriteRune(char) - escapeNext = false + + if state == ESC { + if char == 'u' { + state = UNICODE_4 + hex.Reset() + } else if char == 'U' { + state = UNICODE_8 + hex.Reset() + } else if newChar, ok := escapeSequenceMap[char]; ok { + buffer.WriteRune(newChar) + state = BASIC + } else { + return nil, fmt.Errorf(`invalid escape sequence \%c`, char) + } continue } + + if state == UNICODE_4 || state == UNICODE_8 { + if isHexDigit(char) { + hex.WriteRune(char) + } + if (state == UNICODE_4 && hex.Len() == 4) || (state == UNICODE_8 && hex.Len() == 8) { + if value, err := strconv.ParseInt(hex.String(), 16, 32); err == nil { + buffer.WriteRune(rune(value)) + } else { + return nil, err + } + state = BASIC + } + continue + } + switch char { case '\\': - escapeNext = true - continue - case '"': - if inQuotes { + if state == BASIC { + state = ESC + } else if state == LITERAL { + buffer.WriteRune(char) + } + case '\'': + if state == BARE { + state = LITERAL + } else if state == LITERAL { groups = append(groups, buffer.String()) buffer.Reset() wasInQuotes = true + state = BARE + } + expectDot = false + case '"': + if state == BARE { + state = BASIC + } else if state == BASIC { + groups = append(groups, buffer.String()) + buffer.Reset() + state = BARE + wasInQuotes = true } - inQuotes = !inQuotes expectDot = false case '.': - if inQuotes { + if state != BARE { buffer.WriteRune(char) } else { if !wasInQuotes { @@ -58,28 +122,31 @@ func parseKey(key string) ([]string, error) { wasInQuotes = false } case ' ': - if inQuotes { + if state == BASIC { buffer.WriteRune(char) } else { expectDot = true } default: - if !inQuotes && !isValidBareChar(char) { - return nil, fmt.Errorf("invalid bare character: %c", char) - } - if !inQuotes && expectDot { - return nil, errors.New("what?") + if state == BARE { + if !isValidBareChar(char) { + return nil, fmt.Errorf("invalid bare character: %c", char) + } else if expectDot { + return nil, errors.New("what?") + } } buffer.WriteRune(char) expectDot = false } } - if inQuotes { + + // state must be BARE at the end + if state == ESC { + return nil, errors.New("unfinished escape sequence") + } else if state != BARE { return nil, errors.New("mismatched quotes") } - if escapeNext { - return nil, errors.New("unfinished escape sequence") - } + if buffer.Len() > 0 { groups = append(groups, buffer.String()) } diff --git a/keysparsing_test.go b/keysparsing_test.go index 1a9eccc..7aa4cd6 100644 --- a/keysparsing_test.go +++ b/keysparsing_test.go @@ -22,7 +22,10 @@ func testResult(t *testing.T, key string, expected []string) { } func testError(t *testing.T, key string, expectedError string) { - _, err := parseKey(key) + res, err := parseKey(key) + if err == nil { + t.Fatalf("Expected error, but succesfully parsed key %s", res) + } if fmt.Sprintf("%s", err) != expectedError { t.Fatalf("Expected error \"%s\", but got \"%s\".", expectedError, err) } @@ -47,6 +50,17 @@ func TestBaseKeyPound(t *testing.T) { func TestQuotedKeys(t *testing.T) { testResult(t, `hello."foo".bar`, []string{"hello", "foo", "bar"}) testResult(t, `"hello!"`, []string{"hello!"}) + testResult(t, `"hello\tworld"`, []string{"hello\tworld"}) + testResult(t, `"\U0001F914"`, []string{"\U0001F914"}) + testResult(t, `"\u2764"`, []string{"\u2764"}) + + testResult(t, `hello.'foo'.bar`, []string{"hello", "foo", "bar"}) + testResult(t, `'hello!'`, []string{"hello!"}) + testResult(t, `'hello\tworld'`, []string{`hello\tworld`}) + + testError(t, `"\w"`, `invalid escape sequence \w`) + testError(t, `"\`, `unfinished escape sequence`) + testError(t, `"\t`, `mismatched quotes`) } func TestEmptyKey(t *testing.T) {