Files are small and these are cheap enough that we should be more worried about overflow errors than the space cost.
186 lines
4.2 KiB
Go
186 lines
4.2 KiB
Go
package ssh_config
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type sshParser struct {
|
|
flow chan token
|
|
config *Config
|
|
tokensBuffer []token
|
|
currentTable []string
|
|
seenTableKeys []string
|
|
// /etc/ssh parser or local parser - used to find the default for relative
|
|
// filepaths in the Include directive
|
|
system bool
|
|
depth uint8
|
|
}
|
|
|
|
type sshParserStateFn func() sshParserStateFn
|
|
|
|
// Formats and panics an error message based on a token
|
|
func (p *sshParser) raiseErrorf(tok *token, msg string, args ...interface{}) {
|
|
// TODO this format is ugly
|
|
panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
|
|
}
|
|
|
|
func (p *sshParser) raiseError(tok *token, err error) {
|
|
if err == ErrDepthExceeded {
|
|
panic(err)
|
|
}
|
|
// TODO this format is ugly
|
|
panic(tok.Position.String() + ": " + err.Error())
|
|
}
|
|
|
|
func (p *sshParser) run() {
|
|
for state := p.parseStart; state != nil; {
|
|
state = state()
|
|
}
|
|
}
|
|
|
|
func (p *sshParser) peek() *token {
|
|
if len(p.tokensBuffer) != 0 {
|
|
return &(p.tokensBuffer[0])
|
|
}
|
|
|
|
tok, ok := <-p.flow
|
|
if !ok {
|
|
return nil
|
|
}
|
|
p.tokensBuffer = append(p.tokensBuffer, tok)
|
|
return &tok
|
|
}
|
|
|
|
func (p *sshParser) getToken() *token {
|
|
if len(p.tokensBuffer) != 0 {
|
|
tok := p.tokensBuffer[0]
|
|
p.tokensBuffer = p.tokensBuffer[1:]
|
|
return &tok
|
|
}
|
|
tok, ok := <-p.flow
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &tok
|
|
}
|
|
|
|
func (p *sshParser) parseStart() sshParserStateFn {
|
|
tok := p.peek()
|
|
|
|
// end of stream, parsing is finished
|
|
if tok == nil {
|
|
return nil
|
|
}
|
|
|
|
switch tok.typ {
|
|
case tokenComment, tokenEmptyLine:
|
|
return p.parseComment
|
|
case tokenKey:
|
|
return p.parseKV
|
|
case tokenEOF:
|
|
return nil
|
|
default:
|
|
p.raiseErrorf(tok, fmt.Sprintf("unexpected token %q\n", tok))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *sshParser) parseKV() sshParserStateFn {
|
|
key := p.getToken()
|
|
hasEquals := false
|
|
val := p.getToken()
|
|
if val.typ == tokenEquals {
|
|
hasEquals = true
|
|
val = p.getToken()
|
|
}
|
|
comment := ""
|
|
tok := p.peek()
|
|
if tok == nil {
|
|
tok = &token{typ: tokenEOF}
|
|
}
|
|
if tok.typ == tokenComment && tok.Position.Line == val.Position.Line {
|
|
tok = p.getToken()
|
|
comment = tok.val
|
|
}
|
|
if strings.ToLower(key.val) == "match" {
|
|
// https://github.com/kevinburke/ssh_config/issues/6
|
|
p.raiseErrorf(val, "ssh_config: Match directive parsing is unsupported")
|
|
return nil
|
|
}
|
|
if strings.ToLower(key.val) == "host" {
|
|
strPatterns := strings.Split(val.val, " ")
|
|
patterns := make([]*Pattern, 0)
|
|
for i := range strPatterns {
|
|
if strPatterns[i] == "" {
|
|
continue
|
|
}
|
|
pat, err := NewPattern(strPatterns[i])
|
|
if err != nil {
|
|
p.raiseErrorf(val, "Invalid host pattern: %v", err)
|
|
return nil
|
|
}
|
|
patterns = append(patterns, pat)
|
|
}
|
|
p.config.Hosts = append(p.config.Hosts, &Host{
|
|
Patterns: patterns,
|
|
Nodes: make([]Node, 0),
|
|
EOLComment: comment,
|
|
hasEquals: hasEquals,
|
|
})
|
|
return p.parseStart
|
|
}
|
|
lastHost := p.config.Hosts[len(p.config.Hosts)-1]
|
|
if strings.ToLower(key.val) == "include" {
|
|
inc, err := NewInclude(strings.Split(val.val, " "), hasEquals, key.Position, comment, p.system, p.depth+1)
|
|
if err == ErrDepthExceeded {
|
|
p.raiseError(val, err)
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
p.raiseErrorf(val, "Error parsing Include directive: %v", err)
|
|
return nil
|
|
}
|
|
lastHost.Nodes = append(lastHost.Nodes, inc)
|
|
return p.parseStart
|
|
}
|
|
kv := &KV{
|
|
Key: key.val,
|
|
Value: val.val,
|
|
Comment: comment,
|
|
hasEquals: hasEquals,
|
|
leadingSpace: key.Position.Col - 1,
|
|
position: key.Position,
|
|
}
|
|
lastHost.Nodes = append(lastHost.Nodes, kv)
|
|
return p.parseStart
|
|
}
|
|
|
|
func (p *sshParser) parseComment() sshParserStateFn {
|
|
comment := p.getToken()
|
|
lastHost := p.config.Hosts[len(p.config.Hosts)-1]
|
|
lastHost.Nodes = append(lastHost.Nodes, &Empty{
|
|
Comment: comment.val,
|
|
// account for the "#" as well
|
|
leadingSpace: comment.Position.Col - 2,
|
|
position: comment.Position,
|
|
})
|
|
return p.parseStart
|
|
}
|
|
|
|
func parseSSH(flow chan token, system bool, depth uint8) *Config {
|
|
result := newConfig()
|
|
result.position = Position{1, 1}
|
|
parser := &sshParser{
|
|
flow: flow,
|
|
config: result,
|
|
tokensBuffer: make([]token, 0),
|
|
currentTable: make([]string, 0),
|
|
seenTableKeys: make([]string, 0),
|
|
system: system,
|
|
depth: depth,
|
|
}
|
|
parser.run()
|
|
return result
|
|
}
|