diff --git a/internal/cnf/parser/common.go b/internal/cnf/parser/common.go new file mode 100644 index 0000000..65d9ab3 --- /dev/null +++ b/internal/cnf/parser/common.go @@ -0,0 +1,23 @@ +package parser + +import ( + "io" +) + +// Decoder is the common interface shared by parsers +// cf. encoding/json, etc +type Decoder interface { + Decode(interface{}) error +} + +// Encoder is the common interface shared by parsers +// cf. encoding/json, etc +type Encoder interface { + Encode(interface{}) error +} + +// T is the common parser interface +type T interface { + NewDecoder(io.Reader) Decoder + NewEncoder(io.Writer) Decoder +} diff --git a/internal/cnf/parser/nginx/decoder.go b/internal/cnf/parser/nginx/decoder.go new file mode 100644 index 0000000..74546ca --- /dev/null +++ b/internal/cnf/parser/nginx/decoder.go @@ -0,0 +1,115 @@ +package nginx + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strings" +) + +var ErrNullReceiver = fmt.Errorf("decoder receiver must not be null") +var ErrInvalidReceiver = fmt.Errorf("decoder receiver must be compatible with *[]*Line") + +// decoder implements parser.Decoder +type decoder struct{ reader io.Reader } + +func (d *decoder) Decode(v interface{}) error { + + // check 'v' + if v == nil { + return ErrNullReceiver + } + vcast, ok := v.(*[]*Line) + if !ok { + return ErrInvalidReceiver + } + + r := bufio.NewReader(d.reader) + n := -1 // line number + + // regexes + 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{} + + for { + n++ + + l := &Line{Number: n, Type: NONE} + + // 1. read line + notrim, err := r.ReadString('\n') + if err == io.EOF { + break + } else if err != nil { + return err + } + + // 2. ignore empty + line := strings.Trim(notrim, " \t\r\n") + if len(line) < 1 { + continue + } + + // 3. get indentation + firstChar := 0 + for ; !in_array(" \t\r\n", notrim[firstChar:][0]); firstChar++ { + } + l.Indent = notrim[0:firstChar] + + // 3. comment + if line[0] == '#' { + l.Type = COMMENT + l.Components = []string{strings.Trim(line[1:], " \t")} + } else if line[0] == ';' { + l.Type = COLONCOMMENT + l.Components = []string{strings.Trim(line[1:], " \t")} + } + + // 4. section + if l.Type == NONE { + match := reSection.FindStringSubmatch(line) + if match != nil { + l.Type = SECTION + l.Components = match[1:] + } + } + + // 5. include + if l.Type == NONE { + match := reInclude.FindStringSubmatch(line) + if match != nil { + l.Type = INCLUDE + l.Components = []string{match[1]} + } + } + + // 6. assignment + if l.Type == NONE { + match := reAssign.FindStringSubmatch(line) + if match != nil { + l.Type = ASSIGNMENT + l.Components = match[1:] + } + } + + *vcast = append(*vcast, l) + + } + + return nil + +} + +func in_array(haystack string, needle byte) bool { + for i, l := 0, len(haystack); i < l; i++ { + if haystack[i] == needle { + return true + } + } + return false +} diff --git a/internal/cnf/parser/nginx/decoder_test.go b/internal/cnf/parser/nginx/decoder_test.go new file mode 100644 index 0000000..0235269 --- /dev/null +++ b/internal/cnf/parser/nginx/decoder_test.go @@ -0,0 +1,98 @@ +package nginx + +import ( + "strings" + "testing" +) + +func TestEachLineType(t *testing.T) { + + tests := []struct { + Raw string + Components []string + Type LineType + }{ + {"key value;\n", []string{"key", "value"}, ASSIGNMENT}, + {"key value;\n", []string{"key", "value"}, ASSIGNMENT}, + {"key \t value;\n", []string{"key", "value"}, ASSIGNMENT}, + {"key\tvalue;\n", []string{"key", "value"}, ASSIGNMENT}, + {"ke-y value;\n", []string{"ke-y", "value"}, ASSIGNMENT}, + {"ke_y value;\n", []string{"ke_y", "value"}, ASSIGNMENT}, + {"key value; \n", []string{"key", "value"}, ASSIGNMENT}, + {"key value;\t\n", []string{"key", "value"}, ASSIGNMENT}, + {"\tkey value;\n", []string{"key", "value"}, ASSIGNMENT}, + {" \t key value;\n", []string{"key", "value"}, ASSIGNMENT}, + + {"include ./file/*.conf;\n", []string{"./file/*.conf"}, INCLUDE}, + {"include ./file/*.conf; \n", []string{"./file/*.conf"}, INCLUDE}, + {"include ./file/*.conf;\t\n", []string{"./file/*.conf"}, INCLUDE}, + {"\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}, + + {"#some comment\n", []string{"some comment"}, COMMENT}, + {"#some\tcomment\n", []string{"some\tcomment"}, COMMENT}, + {"# some comment \n", []string{"some comment"}, COMMENT}, + {"# some comment \t\n", []string{"some comment"}, COMMENT}, + {"\t# some comment {\n", []string{"some comment {"}, COMMENT}, + + {";some comment\n", []string{"some comment"}, COLONCOMMENT}, + {"; some\tcomment\n", []string{"some\tcomment"}, COLONCOMMENT}, + {"; some comment\n", []string{"some comment"}, COLONCOMMENT}, + {"; some comment \n", []string{"some comment"}, COLONCOMMENT}, + {"; some comment \t\n", []string{"some comment"}, COLONCOMMENT}, + {"\t; some comment {\n", []string{"some comment {"}, COLONCOMMENT}, + } + + for i, test := range tests { + + // 1. create reader + parser := new(nginx) + decoder := parser.NewDecoder(strings.NewReader(test.Raw)) + + // 2. Decode + receiver := []*Line{} + 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)) + continue + } + + if receiver[0].Type != test.Type { + t.Errorf("[%d] expected type %d, got %d", i, test.Type, receiver[0].Type) + continue + } + + if receiver[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)) + continue + } + + // check each component individually + for c, comp := range receiver[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 + + } + } + + } +} diff --git a/internal/cnf/parser/nginx/encoder.go b/internal/cnf/parser/nginx/encoder.go new file mode 100644 index 0000000..357ba3d --- /dev/null +++ b/internal/cnf/parser/nginx/encoder.go @@ -0,0 +1,26 @@ +package nginx + +import ( + "io" +) + +var ( + defaultPrefix = "" + defaultIndent = " " +) + +// encoder implements parser.Encoder +type encoder struct { + writer io.Writer + prefix string + indent string +} + +func (e *encoder) SetIndent(prefix, indent string) { + e.prefix = prefix + e.indent = indent +} + +func (e *encoder) Encode(v interface{}) error { + return nil +} diff --git a/internal/cnf/parser/nginx/nginx.go b/internal/cnf/parser/nginx/nginx.go new file mode 100644 index 0000000..8c67bd6 --- /dev/null +++ b/internal/cnf/parser/nginx/nginx.go @@ -0,0 +1,46 @@ +package nginx + +import ( + "io" +) + +type LineType byte + +const ( + NONE LineType = iota + COMMENT // # comment + COLONCOMMENT // ; comment + ASSIGNMENT + INCLUDE + SECTION +) + +type Line struct { + // Number of the line in the input file + Number int + + // Type of line + Type LineType + + // Path is the absolute dot-separated path to this line + // "" | at the root of the file + // "a.b" | inside the 'a' section inside the 'a' section + Path string + + // Components of the line + Components []string + + // Children of the current section (nil if not a section) + Children []Line + + // Indent is the indentation characters + Indent string +} + +type nginx struct { + Lines []*Line +} + +func (n *nginx) NewDecoder(r io.Reader) *decoder { + return &decoder{reader: r} +}