feat(wireguard): Add Wireguard parser

This commit is contained in:
Myzel394 2024-08-13 22:37:09 +02:00
parent e8fe8d1a96
commit 2db9371fd0
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
5 changed files with 551 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package wireguard
type malformedLineError struct{}
func (e *malformedLineError) Error() string {
return "Malformed line"
}
type lineError struct {
Line uint32
Err error
}

View File

@ -0,0 +1,117 @@
package wireguard
import (
"regexp"
"slices"
"strings"
)
var commentPattern = regexp.MustCompile(`^\s*(;|#)`)
var emptyLinePattern = regexp.MustCompile(`^\s*$`)
var headerPattern = regexp.MustCompile(`^\s*\[`)
type characterLocation struct {
Start uint32
End uint32
}
type wireguardParser struct {
Sections []wireguardSection
// Used to identify where not to show diagnostics
CommentLines []uint32
}
type lineType string
const (
LineTypeComment lineType = "comment"
LineTypeEmpty lineType = "empty"
LineTypeHeader lineType = "header"
LineTypeProperty lineType = "property"
)
func getLineType(line string) lineType {
if commentPattern.MatchString(line) {
return LineTypeComment
}
if emptyLinePattern.MatchString(line) {
return LineTypeEmpty
}
if headerPattern.MatchString(line) {
return LineTypeHeader
}
return LineTypeProperty
}
func (p *wireguardParser) parseFromString(input string) []lineError {
errors := []lineError{}
lines := strings.Split(
input,
"\n",
)
slices.Reverse(lines)
collectedProperties := wireguardProperties{}
var lastPropertyLine *uint32
for index, line := range lines {
currentLineNumber := uint32(len(lines) - index - 1)
lineType := getLineType(line)
switch lineType {
case LineTypeComment:
p.CommentLines = append(p.CommentLines, currentLineNumber)
case LineTypeEmpty:
continue
case LineTypeProperty:
err := collectedProperties.AddLine(currentLineNumber, line)
if err != nil {
errors = append(errors, lineError{
Line: currentLineNumber,
Err: err,
})
continue
}
if lastPropertyLine == nil {
lastPropertyLine = &currentLineNumber
}
case LineTypeHeader:
var lastLine uint32
if lastPropertyLine == nil {
// Current line
lastLine = currentLineNumber
} else {
lastLine = *lastPropertyLine
}
section := createWireguardSection(
currentLineNumber,
lastLine,
line,
collectedProperties,
)
p.Sections = append(p.Sections, section)
// Reset
collectedProperties = wireguardProperties{}
lastPropertyLine = nil
}
}
// Since we parse the content from bottom to top,
// we need to reverse the order
slices.Reverse(p.CommentLines)
slices.Reverse(p.Sections)
return errors
}

View File

@ -0,0 +1,255 @@
package wireguard
import (
"strings"
"testing"
)
func dedent(s string) string {
return strings.TrimLeft(s, "\n")
}
func TestValidWildTestWorksFine(
t *testing.T,
) {
sample := dedent(`
[Interface]
PrivateKey = 1234567890
Address = 192.168.1.0/24
# I'm a comment
[Peer]
PublicKey = 1234567890
Endpoint = 1.2.3.4 ; I'm just a comment
[Peer]
PublicKey = 5555
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error %v", errors)
}
if !(len(parser.CommentLines) == 1 && parser.CommentLines[0] == 4) {
t.Fatalf("parseFromString failed to collect comment lines %v", parser.CommentLines)
}
if !((len(parser.Sections) == 3) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer") && (*parser.Sections[2].Name == "Peer")) {
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
}
if !(parser.Sections[0].StartLine == 0 && parser.Sections[0].EndLine == 2 && parser.Sections[1].StartLine == 5 && parser.Sections[1].EndLine == 7 && parser.Sections[2].StartLine == 9 && parser.Sections[2].EndLine == 10) {
t.Fatalf("parseFromString: Invalid start and end lines %v", parser.Sections)
}
if !((len(parser.Sections[0].Properties) == 2) && (len(parser.Sections[1].Properties) == 2) && (len(parser.Sections[2].Properties) == 1)) {
t.Fatalf("parseFromString: Invalid amount of properties %v", parser.Sections)
}
if !((parser.Sections[0].Properties[1].Key.Name == "PrivateKey") && (parser.Sections[0].Properties[2].Key.Name == "Address")) {
t.Fatalf("parseFromString failed to collect properties of section 0 %v", parser.Sections[0].Properties)
}
if !((parser.Sections[1].Properties[6].Key.Name == "PublicKey") && (parser.Sections[1].Properties[7].Key.Name == "Endpoint")) {
t.Fatalf("parseFromString failed to collect properties of section 1 %v", parser.Sections[1].Properties)
}
if !(parser.Sections[2].Properties[10].Key.Name == "PublicKey") {
t.Fatalf("parseFromString failed to collect properties of section 2 %v", parser.Sections[2].Properties)
}
}
func TestEmptySectionAtStartWorksFine(
t *testing.T,
) {
sample := dedent(`
[Interface]
[Peer]
PublicKey = 1234567890
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error %v", errors)
}
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer")) {
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
}
if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) {
t.Fatalf("parseFromString failed to collect properties %v", parser.Sections)
}
}
func TestEmptySectionAtEndWorksFine(
t *testing.T,
) {
sample := dedent(`
[Inteface]
PrivateKey = 1234567890
[Peer]
# Just sneaking in here, hehe
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error %v", errors)
}
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inteface") && (*parser.Sections[1].Name == "Peer")) {
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
}
if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) {
t.Fatalf("parseFromString failed to collect properties %v", parser.Sections)
}
if !(len(parser.CommentLines) == 1 && parser.CommentLines[0] == 4) {
t.Fatalf("parseFromString failed to collect comment lines %v", parser.CommentLines)
}
}
func TestEmptyFileWorksFine(
t *testing.T,
) {
sample := dedent(`
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error %v", errors)
}
if !(len(parser.Sections) == 0) {
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
}
}
func TestPartialSectionWithNoPropertiesWorksFine(
t *testing.T,
) {
sample := dedent(`
[Inte
[Peer]
PublicKey = 1234567890
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error %v", errors)
}
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) {
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
}
if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) {
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections)
}
if !(len(parser.CommentLines) == 0) {
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.CommentLines)
}
if !(parser.Sections[1].Properties[3].Key.Name == "PublicKey") {
t.Fatalf("parseFromString failed to collect properties of section 1: %v", parser.Sections[1].Properties)
}
}
func TestPartialSectionWithPropertiesWorksFine(
t *testing.T,
) {
sample := dedent(`
[Inte
PrivateKey = 1234567890
[Peer]
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error: %v", errors)
}
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) {
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
}
if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) {
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections)
}
if !(parser.Sections[0].Properties[1].Key.Name == "PrivateKey") {
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
}
}
func TestFileWithOnlyComments(
t *testing.T,
) {
sample := dedent(`
# This is a comment
# Another comment
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error: %v", errors)
}
if !(len(parser.Sections) == 0) {
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
}
if !(len(parser.CommentLines) == 2) {
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.CommentLines)
}
if !(parser.CommentLines[0] == 0 && parser.CommentLines[1] == 1) {
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.CommentLines)
}
}
func TestMultipleSectionsNoProperties(
t *testing.T,
) {
sample := dedent(`
[Interface]
[Peer]
[Peer]
`)
parser := wireguardParser{}
errors := parser.parseFromString(sample)
if len(errors) > 0 {
t.Fatalf("parseFromString failed with error: %v", errors)
}
if !(len(parser.Sections) == 3) {
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
}
for _, section := range parser.Sections {
if len(section.Properties) != 0 {
t.Fatalf("parseFromString failed to collect properties: %v", section.Properties)
}
}
}

View File

@ -0,0 +1,119 @@
package wireguard
import (
"config-lsp/utils"
"regexp"
"strings"
)
var linePattern = regexp.MustCompile(`^\s*(?P<key>.+?)\s*(?P<separator>=)\s*(?P<value>\S.*?)?\s*(?:(?:;|#).*)?\s*$`)
type wireguardPropertyKey struct {
Location characterLocation
Name string
}
type wireguardPropertyValue struct {
Location characterLocation
Value string
}
type wireguardPropertySeparator struct {
Location characterLocation
}
type wireguardProperty struct {
Key wireguardPropertyKey
Separator *wireguardPropertySeparator
Value *wireguardPropertyValue
}
func (p wireguardProperty) String() string {
if p.Value == nil {
return p.Key.Name
}
return p.Key.Name + "=" + p.Value.Value
}
func createWireguardProperty(line string) (*wireguardProperty, error) {
if !strings.Contains(line, "=") {
indexes := utils.GetTrimIndex(line)
if indexes == nil {
// weird, should not happen
return nil, &malformedLineError{}
}
return &wireguardProperty{
Key: wireguardPropertyKey{
Location: characterLocation{
Start: uint32(indexes[0]),
End: uint32(indexes[1]),
},
},
}, nil
}
indexes := linePattern.FindStringSubmatchIndex(line)
if indexes == nil || len(indexes) == 0 {
return nil, &malformedLineError{}
}
keyStart := uint32(indexes[2])
keyEnd := uint32(indexes[3])
key := wireguardPropertyKey{
Location: characterLocation{
Start: keyStart,
End: keyEnd,
},
Name: line[keyStart:keyEnd],
}
separatorStart := uint32(indexes[4])
separatorEnd := uint32(indexes[5])
separator := wireguardPropertySeparator{
Location: characterLocation{
Start: separatorStart,
End: separatorEnd,
},
}
var value *wireguardPropertyValue
if len(indexes) > 6 {
// value exists
valueStart := uint32(indexes[6])
valueEnd := uint32(indexes[7])
value = &wireguardPropertyValue{
Location: characterLocation{
Start: valueStart,
End: valueEnd,
},
Value: line[valueStart:valueEnd],
}
}
return &wireguardProperty{
Key: key,
Separator: &separator,
Value: value,
}, nil
}
// [<line number>]: <property>
type wireguardProperties map[uint32]wireguardProperty
func (p *wireguardProperties) AddLine(lineNumber uint32, line string) error {
property, err := createWireguardProperty(line)
if err != nil {
return err
}
(*p)[lineNumber] = *property
return nil
}

View File

@ -0,0 +1,48 @@
package wireguard
import (
"fmt"
"regexp"
)
type wireguardSection struct {
StartLine uint32
EndLine uint32
// nil = do not belong to a section
Name *string
Properties wireguardProperties
}
func (s wireguardSection) String() string {
var name string
if s.Name == nil {
name = "//<nil>//"
} else {
name = *s.Name
}
return fmt.Sprintf("[%s]; %d-%d: %v", name, s.StartLine, s.EndLine, s.Properties)
}
var validHeaderPattern = regexp.MustCompile(`^\s*\[(?P<header>.+?)\]\s*$`)
func createWireguardSection(startLine uint32, endLine uint32, headerLine string, props wireguardProperties) wireguardSection {
match := validHeaderPattern.FindStringSubmatch(headerLine)
var header string
if match == nil {
// Still typing it
header = headerLine[1:]
} else {
header = match[1]
}
return wireguardSection{
StartLine: startLine,
EndLine: endLine,
Name: &header,
Properties: props,
}
}