diff --git a/handlers/ssh_config/ast/listener.go b/handlers/ssh_config/ast/listener.go index 3adfe6e..a9cc359 100644 --- a/handlers/ssh_config/ast/listener.go +++ b/handlers/ssh_config/ast/listener.go @@ -4,6 +4,7 @@ import ( "config-lsp/common" commonparser "config-lsp/common/parser" "config-lsp/handlers/ssh_config/ast/parser" + hostparser "config-lsp/handlers/ssh_config/host-parser" "config-lsp/handlers/ssh_config/match-parser" "strings" @@ -131,7 +132,7 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) { if len(errors) > 0 { for _, err := range errors { s.Errors = append(s.Errors, common.LSPError{ - Range: err.Range.ShiftHorizontal(s.sshContext.currentOption.Start.Character), + Range: err.Range, Err: err.Err, }) } @@ -153,6 +154,41 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) { s.sshContext.currentKeyIsBlockOf = nil s.sshContext.currentBlock = matchBlock + case SSHBlockTypeHost: + var host *hostparser.Host + + hostParser := hostparser.NewHost() + errors := hostParser.Parse( + s.sshContext.currentOption.OptionValue.Value.Raw, + location.Start.Line, + s.sshContext.currentOption.OptionValue.Start.Character, + ) + + if len(errors) > 0 { + for _, err := range errors { + s.Errors = append(s.Errors, common.LSPError{ + Range: err.Range, + Err: err.Err, + }) + } + } else { + host = hostParser + } + + hostBlock := &SSHHostBlock{ + LocationRange: location, + HostOption: s.sshContext.currentOption, + HostValue: host, + Options: treemap.NewWith(gods.UInt32Comparator), + } + + s.Config.Options.Put( + location.Start.Line, + hostBlock, + ) + + s.sshContext.currentKeyIsBlockOf = nil + s.sshContext.currentBlock = hostBlock } return diff --git a/handlers/ssh_config/ast/parser_test.go b/handlers/ssh_config/ast/parser_test.go index dba0dc3..791d25f 100644 --- a/handlers/ssh_config/ast/parser_test.go +++ b/handlers/ssh_config/ast/parser_test.go @@ -127,3 +127,132 @@ Match originalhost "192.168.0.1" t.Errorf("Expected third entry to be User root, but got: %v", thirdEntry) } } + +func TestSimpleHostBlock( + t *testing.T, +) { + input := utils.Dedent(` +Ciphers 3des-cbc + +Host example.com + Port 22 +`) + p := NewSSHConfig() + errors := p.Parse(input) + + if len(errors) != 0 { + t.Fatalf("Expected no errors, got %v", errors) + } + + if !(p.Options.Size() == 2 && + len(utils.KeysOfMap(p.CommentLines)) == 0) { + t.Errorf("Expected 2 option and no comment lines, but got: %v entries, %v comment lines", p.Options.Size(), len(p.CommentLines)) + } + + rawFirstEntry, _ := p.Options.Get(uint32(0)) + firstEntry := rawFirstEntry.(*SSHOption) + if !(firstEntry.Value.Value == "Ciphers 3des-cbc") { + t.Errorf("Expected first entry to be Ciphers 3des-cbc, but got: %v", firstEntry) + } + + rawSecondEntry, _ := p.Options.Get(uint32(2)) + secondEntry := rawSecondEntry.(*SSHHostBlock) + if !(secondEntry.Options.Size() == 1 && + secondEntry.LocationRange.Start.Line == 2 && + secondEntry.LocationRange.Start.Character == 0 && + secondEntry.LocationRange.End.Line == 3 && + secondEntry.LocationRange.End.Character == 8) { + t.Errorf("Expected second entry to be Host example.com, but got: %v", secondEntry) + } + + rawThirdEntry, _ := secondEntry.Options.Get(uint32(3)) + thirdEntry := rawThirdEntry.(*SSHOption) + if !(thirdEntry.Value.Raw == "\tPort 22" && + thirdEntry.Key.Value.Raw == "Port" && + thirdEntry.OptionValue.Value.Raw == "22" && + thirdEntry.LocationRange.Start.Line == 3 && + thirdEntry.LocationRange.Start.Character == 0 && + thirdEntry.LocationRange.End.Line == 3 && + thirdEntry.LocationRange.End.Character == 8) { + t.Errorf("Expected third entry to be Port 22, but got: %v", thirdEntry) + } + + rawFourthEntry, _ := p.Options.Get(uint32(3)) + + if !(rawFourthEntry == nil) { + t.Errorf("Expected fourth entry to be nil, but got: %v", rawFourthEntry) + } +} + +func TestComplexExample( + t *testing.T, +) { + input := utils.Dedent(` +Host laptop + HostName laptop.lan + +Match originalhost laptop exec "[[ $(/usr/bin/dig +short laptop.lan) == '' ]]" + HostName laptop.sdn +`) + p := NewSSHConfig() + errors := p.Parse(input) + + if len(errors) != 0 { + t.Fatalf("Expected no errors, got %v", errors) + } + + if !(p.Options.Size() == 2 && + len(utils.KeysOfMap(p.CommentLines)) == 0) { + t.Errorf("Expected 2 option and no comment lines, but got: %v entries, %v comment lines", p.Options.Size(), len(p.CommentLines)) + } + + rawFirstEntry, _ := p.Options.Get(uint32(0)) + firstBlock := rawFirstEntry.(*SSHHostBlock) + if !(firstBlock.Options.Size() == 1 && + firstBlock.LocationRange.Start.Line == 0 && + firstBlock.LocationRange.Start.Character == 0 && + firstBlock.LocationRange.End.Line == 1 && + firstBlock.LocationRange.End.Character == 23) { + t.Errorf("Expected first entry to be Host laptop, but got: %v", firstBlock) + } + + rawSecondEntry, _ := firstBlock.Options.Get(uint32(1)) + secondOption := rawSecondEntry.(*SSHOption) + if !(secondOption.Value.Raw == " HostName laptop.lan" && + secondOption.Key.Value.Raw == "HostName" && + secondOption.OptionValue.Value.Raw == "laptop.lan" && + secondOption.LocationRange.Start.Line == 1 && + secondOption.LocationRange.Start.Character == 0 && + secondOption.Key.LocationRange.Start.Character == 4 && + secondOption.LocationRange.End.Line == 1 && + secondOption.LocationRange.End.Character == 23) { + t.Errorf("Expected second entry to be HostName laptop.lan, but got: %v", secondOption) + } + + rawThirdEntry, _ := p.Options.Get(uint32(3)) + secondBlock := rawThirdEntry.(*SSHMatchBlock) + if !(secondBlock.Options.Size() == 1 && + secondBlock.LocationRange.Start.Line == 3 && + secondBlock.LocationRange.Start.Character == 0 && + secondBlock.LocationRange.End.Line == 4 && + secondBlock.LocationRange.End.Character == 23) { + t.Errorf("Expected second entry to be Match originalhost laptop exec \"[[ $(/usr/bin/dig +short laptop.lan) == '' ]]\", but got: %v", secondBlock) + } + + if !(secondBlock.MatchOption.LocationRange.End.Character == 78) { + t.Errorf("Expected second entry to be Match originalhost laptop exec \"[[ $(/usr/bin/dig +short laptop.lan) == '' ]]\", but got: %v", secondBlock) + } + + rawFourthEntry, _ := secondBlock.Options.Get(uint32(4)) + thirdOption := rawFourthEntry.(*SSHOption) + if !(thirdOption.Value.Raw == " HostName laptop.sdn" && + thirdOption.Key.Value.Raw == "HostName" && + thirdOption.OptionValue.Value.Raw == "laptop.sdn" && + thirdOption.LocationRange.Start.Line == 4 && + thirdOption.LocationRange.Start.Character == 0 && + thirdOption.Key.LocationRange.Start.Character == 4 && + thirdOption.LocationRange.End.Line == 4 && + thirdOption.LocationRange.End.Character == 23) { + t.Errorf("Expected third entry to be HostName laptop.sdn, but got: %v", thirdOption) + } +} diff --git a/handlers/ssh_config/ast/ssh_config.go b/handlers/ssh_config/ast/ssh_config.go index 5757282..c9f6120 100644 --- a/handlers/ssh_config/ast/ssh_config.go +++ b/handlers/ssh_config/ast/ssh_config.go @@ -3,7 +3,9 @@ package ast import ( "config-lsp/common" commonparser "config-lsp/common/parser" + hostparser "config-lsp/handlers/ssh_config/host-parser" "config-lsp/handlers/ssh_config/match-parser" + "github.com/emirpasic/gods/maps/treemap" ) @@ -44,10 +46,10 @@ type SSHMatchBlock struct { type SSHHostBlock struct { common.LocationRange HostOption *SSHOption - HostValue string + HostValue *hostparser.Host // [uint32]*SSHOption -> line number -> *SSHOption - Others *treemap.Map + Options *treemap.Map } type SSHConfig struct { diff --git a/handlers/ssh_config/ast/ssh_config_fields.go b/handlers/ssh_config/ast/ssh_config_fields.go index 30cf8d5..55e0960 100644 --- a/handlers/ssh_config/ast/ssh_config_fields.go +++ b/handlers/ssh_config/ast/ssh_config_fields.go @@ -32,7 +32,7 @@ func (b *SSHHostBlock) GetBlockType() SSHBlockType { } func (b *SSHHostBlock) AddOption(option *SSHOption) { - b.Others.Put(option.LocationRange.Start.Line, option) + b.Options.Put(option.LocationRange.Start.Line, option) } func (b *SSHHostBlock) SetEnd(end common.Location) { diff --git a/handlers/ssh_config/host-parser/host_ast.go b/handlers/ssh_config/host-parser/host_ast.go new file mode 100644 index 0000000..28da9ce --- /dev/null +++ b/handlers/ssh_config/host-parser/host_ast.go @@ -0,0 +1,16 @@ +package hostparser + +import ( + "config-lsp/common" + commonparser "config-lsp/common/parser" +) + +type Host struct { + Hosts []*HostValue +} + +type HostValue struct { + common.LocationRange + Value commonparser.ParsedString +} + diff --git a/handlers/ssh_config/host-parser/parser.go b/handlers/ssh_config/host-parser/parser.go new file mode 100644 index 0000000..d2d6a94 --- /dev/null +++ b/handlers/ssh_config/host-parser/parser.go @@ -0,0 +1,54 @@ +package hostparser + +import ( + "config-lsp/common" + "regexp" + commonparser "config-lsp/common/parser" +) + +func NewHost() *Host { + match := new(Host) + match.Clear() + + return match +} + +func (h *Host) Clear() { + h.Hosts = make([]*HostValue, 0) +} + +var textPattern = regexp.MustCompile(`\S+`) + +func (h *Host) Parse( + input string, + line uint32, + startCharacter uint32, +) []common.LSPError { + hostsIndexes := textPattern.FindAllStringIndex(input, -1) + + for _, hostIndex := range hostsIndexes { + startIndex := hostIndex[0] + endIndex := hostIndex[1] + + rawHost := input[startIndex:endIndex] + + value := commonparser.ParseRawString(rawHost, commonparser.FullFeatures) + host := HostValue{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: line, + Character: startCharacter + uint32(startIndex), + }, + End: common.Location{ + Line: line, + Character: startCharacter + uint32(endIndex), + }, + }, + Value: value, + } + + h.Hosts = append(h.Hosts, &host) + } + + return nil +} diff --git a/handlers/ssh_config/host-parser/parser_test.go b/handlers/ssh_config/host-parser/parser_test.go new file mode 100644 index 0000000..d751e7b --- /dev/null +++ b/handlers/ssh_config/host-parser/parser_test.go @@ -0,0 +1,73 @@ +package hostparser + +import "testing" + +func TestSimpleExample( + t *testing.T, +) { + input := `example.com` + + host := NewHost() + offset := uint32(8) + errs := host.Parse(input, 4, offset) + + if len(errs) > 0 { + t.Fatalf("Expected no errors, got %v", errs) + } + + if !(len(host.Hosts) == 1) { + t.Errorf("Expected 1 host, got %v", len(host.Hosts)) + } + + if !(host.Hosts[0].Value.Raw == "example.com") { + t.Errorf("Expected host to be 'example.com', got %v", host.Hosts[0].Value.Raw) + } + + if !(host.Hosts[0].Start.Line == 4 && host.Hosts[0].Start.Character == 0+offset && host.Hosts[0].End.Character == 11+offset) { + t.Errorf("Expected host to be at line 4, characters 0-11, got %v", host.Hosts[0]) + } + + if !(host.Hosts[0].Value.Value == "example.com") { + t.Errorf("Expected host value to be 'example.com', got %v", host.Hosts[0].Value.Value) + } +} + +func TestMultipleExample( + t *testing.T, +) { + input := `example.com example.org example.net` + + host := NewHost() + offset := uint32(8) + errs := host.Parse(input, 4, offset) + + if len(errs) > 0 { + t.Fatalf("Expected no errors, got %v", errs) + } + + if !(len(host.Hosts) == 3) { + t.Errorf("Expected 3 hosts, got %v", len(host.Hosts)) + } +} + +func TestIncompleteExample( + t *testing.T, +) { + input := `example.com ` + + host := NewHost() + offset := uint32(8) + errs := host.Parse(input, 4, offset) + + if len(errs) > 0 { + t.Fatalf("Expected no errors, got %v", errs) + } + + if !(len(host.Hosts) == 1) { + t.Errorf("Expected 1 hosts, got %v", len(host.Hosts)) + } + + if !(host.Hosts[0].Value.Raw == "example.com") { + t.Errorf("Expected host to be 'example.com', got %v", host.Hosts[0].Value.Raw) + } +} diff --git a/handlers/sshd_config/ast/listener.go b/handlers/sshd_config/ast/listener.go index 2ffcdec..cc5a414 100644 --- a/handlers/sshd_config/ast/listener.go +++ b/handlers/sshd_config/ast/listener.go @@ -126,7 +126,7 @@ func (s *sshdParserListener) ExitEntry(ctx *parser.EntryContext) { if len(errors) > 0 { for _, err := range errors { s.Errors = append(s.Errors, common.LSPError{ - Range: err.Range.ShiftHorizontal(s.sshdContext.currentOption.Start.Character), + Range: err.Range, Err: err.Err, }) }