diff --git a/internal/cnf/parser/nginx/decoder.go b/internal/cnf/parser/nginx/decoder.go index 74546ca..21f981e 100644 --- a/internal/cnf/parser/nginx/decoder.go +++ b/internal/cnf/parser/nginx/decoder.go @@ -8,8 +8,11 @@ import ( "strings" ) -var ErrNullReceiver = fmt.Errorf("decoder receiver must not be null") -var ErrInvalidReceiver = fmt.Errorf("decoder receiver must be compatible with *[]*Line") +var ErrNullReceiver = fmt.Errorf("receiver must not be null") +var ErrInvalidReceiver = fmt.Errorf("receiver must be compatible with *[]*Line") +var ErrUnexpectedSectionClose = fmt.Errorf("unexpected section close") +var ErrUnclosedSection = fmt.Errorf("unclosed section") +var ErrInvalidSyntax = fmt.Errorf("invalid syntax") // decoder implements parser.Decoder type decoder struct{ reader io.Reader } @@ -20,21 +23,22 @@ func (d *decoder) Decode(v interface{}) error { if v == nil { return ErrNullReceiver } - vcast, ok := v.(*[]*Line) + vcast, ok := v.(*nginx) if !ok { return ErrInvalidReceiver } + vcast.Lines = make([]*Line, 0) r := bufio.NewReader(d.reader) n := -1 // line number // regexes - reSection := regexp.MustCompile(`(?m)^([a-z0-9_-]+)\s+\{$`) + reSection := regexp.MustCompile(`(?m)^([a-z0-9_-]+)\s*\{$`) reAssign := regexp.MustCompile(`(?m)^([a-z0-9_-]+)\s+([^;#]+);$`) reInclude := regexp.MustCompile(`(?m)^include\s+([^;#]+);$`) - // actual section tree - // tree := []Line{} + // actual section stack + stack := make([]*Line, 0) for { n++ @@ -46,18 +50,19 @@ func (d *decoder) Decode(v interface{}) error { if err == io.EOF { break } else if err != nil { - return err + return &LineError{n, err} } + notrim = strings.Trim(notrim, "\r\n") // 2. ignore empty - line := strings.Trim(notrim, " \t\r\n") + line := strings.Trim(notrim, " \t") if len(line) < 1 { continue } // 3. get indentation - firstChar := 0 - for ; !in_array(" \t\r\n", notrim[firstChar:][0]); firstChar++ { + var firstChar int + for firstChar = 0; in_array(" \t", notrim[firstChar]); firstChar++ { } l.Indent = notrim[0:firstChar] @@ -68,6 +73,11 @@ func (d *decoder) Decode(v interface{}) error { } else if line[0] == ';' { l.Type = COLONCOMMENT l.Components = []string{strings.Trim(line[1:], " \t")} + } else if line[0] == '}' { + l.Type = SECTIONEND + if len(stack) < 1 { + return &LineError{n, ErrUnexpectedSectionClose} + } } // 4. section @@ -76,6 +86,8 @@ func (d *decoder) Decode(v interface{}) error { if match != nil { l.Type = SECTION l.Components = match[1:] + l.Lines = make([]*Line, 0) + stack = append(stack, l) } } @@ -97,8 +109,45 @@ func (d *decoder) Decode(v interface{}) error { } } - *vcast = append(*vcast, l) + // 7. invalid type + if l.Type == NONE { + return &LineError{n, ErrInvalidSyntax} + } + // 8. pop section + if l.Type == SECTIONEND { + cur := stack[len(stack)-1] // pop + stack = stack[0 : len(stack)-1] // pop + + var last []*Line + if len(stack) > 0 { + last = stack[len(stack)-1].Lines + } else { + last = vcast.Lines + } + last = append(last, cur) + + continue + } + + // 9. add to section / or receiver + stacklen := len(stack) + if stacklen > 0 && l.Type == SECTION { + stacklen-- + } + + if stacklen > 0 { + tail := stack[stacklen-1] + tail.Lines = append(tail.Lines, l) + } else { + vcast.Lines = append(vcast.Lines, l) + } + + } + + // fail on unclosed section + if len(stack) > 0 { + return ErrUnclosedSection } return nil diff --git a/internal/cnf/parser/nginx/decoder_test.go b/internal/cnf/parser/nginx/decoder_test.go index 0235269..aac16d0 100644 --- a/internal/cnf/parser/nginx/decoder_test.go +++ b/internal/cnf/parser/nginx/decoder_test.go @@ -29,13 +29,13 @@ func TestEachLineType(t *testing.T) { {"\tinclude ./file/*.conf;\n", []string{"./file/*.conf"}, INCLUDE}, {" \t include ./file/*.conf;\n", []string{"./file/*.conf"}, INCLUDE}, - {"sectionname {\n", []string{"sectionname"}, SECTION}, - {"section-name {\n", []string{"section-name"}, SECTION}, - {"section_name {\n", []string{"section_name"}, SECTION}, - {"sectionname { \n", []string{"sectionname"}, SECTION}, - {"sectionname {\t\n", []string{"sectionname"}, SECTION}, - {"\tsectionname {\n", []string{"sectionname"}, SECTION}, - {" \t sectionname {\n", []string{"sectionname"}, SECTION}, + {"sectionname {\n}\n", []string{"sectionname"}, SECTION}, + {"section-name {\n}\n", []string{"section-name"}, SECTION}, + {"section_name {\n}\n", []string{"section_name"}, SECTION}, + {"sectionname { \n}\n", []string{"sectionname"}, SECTION}, + {"sectionname {\t\n}\n", []string{"sectionname"}, SECTION}, + {"\tsectionname {\n}\n", []string{"sectionname"}, SECTION}, + {" \t sectionname {\n}\n", []string{"sectionname"}, SECTION}, {"#some comment\n", []string{"some comment"}, COMMENT}, {"#some\tcomment\n", []string{"some\tcomment"}, COMMENT}, @@ -58,41 +58,106 @@ func TestEachLineType(t *testing.T) { decoder := parser.NewDecoder(strings.NewReader(test.Raw)) // 2. Decode - receiver := []*Line{} - err := decoder.Decode(&receiver) + receiver := new(nginx) + err := decoder.Decode(receiver) if err != nil { t.Errorf("[%d] unexpected error <%s>", i, err) continue } - if len(receiver) != 1 { - t.Errorf("[%d] expected only 1 element, got %d", i, len(receiver)) + if len(receiver.Lines) != 1 { + t.Errorf("[%d] expected only 1 element, got %d", i, len(receiver.Lines)) continue } - if receiver[0].Type != test.Type { - t.Errorf("[%d] expected type %d, got %d", i, test.Type, receiver[0].Type) + if receiver.Lines[0].Type != test.Type { + t.Errorf("[%d] expected type %d, got %d", i, test.Type, receiver.Lines[0].Type) continue } - if receiver[0].Components == nil && test.Components != nil { + if receiver.Lines[0].Components == nil && test.Components != nil { t.Errorf("[%d] expected components not to be null", i) continue } - if len(receiver[0].Components) != len(test.Components) { - t.Errorf("[%d] expected %d components, got %d", i, len(test.Components), len(receiver[0].Components)) + if len(receiver.Lines[0].Components) != len(test.Components) { + t.Errorf("[%d] expected %d components, got %d", i, len(test.Components), len(receiver.Lines[0].Components)) continue } // check each component individually - for c, comp := range receiver[0].Components { + for c, comp := range receiver.Lines[0].Components { if comp != test.Components[c] { t.Errorf("[%d] expected component %d to be '%s', got '%s'", i, c, test.Components[c], comp) - continue - } } } } + +func TestNestedSections(t *testing.T) { + + tests := []struct { + Raw string + SectionChain []string + }{ + + {"a{\n}\n", []string{"a"}}, + {"a{\n\n}\n", []string{"a"}}, + {"a{\n; some comment\n}\n", []string{"a"}}, + {"a{\n; some comment\n #another comment\n}\n", []string{"a"}}, + {"a{\nb{\n}\n}\n", []string{"a", "b"}}, + {"a{\n\tb{\n\t}\n}\n", []string{"a", "b"}}, + {"a{\n\tb{\n\t\tc{\n\t\t}\n\t}\n}\n", []string{"a", "b", "c"}}, + } + + for i, test := range tests { + + // 1. create reader + parser := new(nginx) + decoder := parser.NewDecoder(strings.NewReader(test.Raw)) + + // 2. Decode + receiver := new(nginx) + err := decoder.Decode(receiver) + + if err != nil { + t.Errorf("[%d] unexpected error <%s>", i, err) + continue + } + + if len(receiver.Lines) != 1 { + t.Errorf("[%d] expected only 1 element, got %d", i, len(receiver.Lines)) + continue + } + + if receiver.Lines[0].Type != SECTION { + t.Errorf("[%d] expected type %d, got %d", i, SECTION, receiver.Lines[0].Type) + continue + } + + // check each component individually + var current []*Line = receiver.Lines + for s, sec := range test.SectionChain { + + // check that section exists in + found := false + for _, line := range current { + + // found + if line.Type == SECTION && line.Components[0] == sec { + found = true + current = line.Lines + break + } + } + + if !found { + t.Errorf("[%d] key '%s' not found\n", i, strings.Join(test.SectionChain[:s+1], "/")) + break + } + + } + + } +} diff --git a/internal/cnf/parser/nginx/errors.go b/internal/cnf/parser/nginx/errors.go new file mode 100644 index 0000000..d80bf7e --- /dev/null +++ b/internal/cnf/parser/nginx/errors.go @@ -0,0 +1,17 @@ +package nginx + +import ( + "fmt" + "git.xdrm.io/go/nix-amer/internal/clifmt" +) + +// LineError wraps errors with a line index +type LineError struct { + Line int + Err error +} + +// Error implements Error +func (le LineError) Error() string { + return fmt.Sprintf(":%d %s", le.Line, clifmt.Color(31, le.Err.Error())) +} diff --git a/internal/cnf/parser/nginx/nginx.go b/internal/cnf/parser/nginx/nginx.go index 8c67bd6..11e9e59 100644 --- a/internal/cnf/parser/nginx/nginx.go +++ b/internal/cnf/parser/nginx/nginx.go @@ -13,6 +13,7 @@ const ( ASSIGNMENT INCLUDE SECTION + SECTIONEND ) type Line struct { @@ -30,8 +31,8 @@ type Line struct { // Components of the line Components []string - // Children of the current section (nil if not a section) - Children []Line + // Lines children of the current section (nil if not a section) + Lines []*Line // Indent is the indentation characters Indent string