1package ssh_config
2
3import (
4	"fmt"
5	"strings"
6)
7
8type sshParser struct {
9	flow          chan token
10	config        *Config
11	tokensBuffer  []token
12	currentTable  []string
13	seenTableKeys []string
14	// /etc/ssh parser or local parser - used to find the default for relative
15	// filepaths in the Include directive
16	system bool
17	depth  uint8
18}
19
20type sshParserStateFn func() sshParserStateFn
21
22// Formats and panics an error message based on a token
23func (p *sshParser) raiseErrorf(tok *token, msg string, args ...interface{}) {
24	// TODO this format is ugly
25	panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
26}
27
28func (p *sshParser) raiseError(tok *token, err error) {
29	if err == ErrDepthExceeded {
30		panic(err)
31	}
32	// TODO this format is ugly
33	panic(tok.Position.String() + ": " + err.Error())
34}
35
36func (p *sshParser) run() {
37	for state := p.parseStart; state != nil; {
38		state = state()
39	}
40}
41
42func (p *sshParser) peek() *token {
43	if len(p.tokensBuffer) != 0 {
44		return &(p.tokensBuffer[0])
45	}
46
47	tok, ok := <-p.flow
48	if !ok {
49		return nil
50	}
51	p.tokensBuffer = append(p.tokensBuffer, tok)
52	return &tok
53}
54
55func (p *sshParser) getToken() *token {
56	if len(p.tokensBuffer) != 0 {
57		tok := p.tokensBuffer[0]
58		p.tokensBuffer = p.tokensBuffer[1:]
59		return &tok
60	}
61	tok, ok := <-p.flow
62	if !ok {
63		return nil
64	}
65	return &tok
66}
67
68func (p *sshParser) parseStart() sshParserStateFn {
69	tok := p.peek()
70
71	// end of stream, parsing is finished
72	if tok == nil {
73		return nil
74	}
75
76	switch tok.typ {
77	case tokenComment, tokenEmptyLine:
78		return p.parseComment
79	case tokenKey:
80		return p.parseKV
81	case tokenEOF:
82		return nil
83	default:
84		p.raiseErrorf(tok, fmt.Sprintf("unexpected token %q\n", tok))
85	}
86	return nil
87}
88
89func (p *sshParser) parseKV() sshParserStateFn {
90	key := p.getToken()
91	hasEquals := false
92	val := p.getToken()
93	if val.typ == tokenEquals {
94		hasEquals = true
95		val = p.getToken()
96	}
97	comment := ""
98	tok := p.peek()
99	if tok == nil {
100		tok = &token{typ: tokenEOF}
101	}
102	if tok.typ == tokenComment && tok.Position.Line == val.Position.Line {
103		tok = p.getToken()
104		comment = tok.val
105	}
106	if strings.ToLower(key.val) == "match" {
107		// https://github.com/kevinburke/ssh_config/issues/6
108		p.raiseErrorf(val, "ssh_config: Match directive parsing is unsupported")
109		return nil
110	}
111	if strings.ToLower(key.val) == "host" {
112		strPatterns := strings.Split(val.val, " ")
113		patterns := make([]*Pattern, 0)
114		for i := range strPatterns {
115			if strPatterns[i] == "" {
116				continue
117			}
118			pat, err := NewPattern(strPatterns[i])
119			if err != nil {
120				p.raiseErrorf(val, "Invalid host pattern: %v", err)
121				return nil
122			}
123			patterns = append(patterns, pat)
124		}
125		p.config.Hosts = append(p.config.Hosts, &Host{
126			Patterns:   patterns,
127			Nodes:      make([]Node, 0),
128			EOLComment: comment,
129			hasEquals:  hasEquals,
130		})
131		return p.parseStart
132	}
133	lastHost := p.config.Hosts[len(p.config.Hosts)-1]
134	if strings.ToLower(key.val) == "include" {
135		inc, err := NewInclude(strings.Split(val.val, " "), hasEquals, key.Position, comment, p.system, p.depth+1)
136		if err == ErrDepthExceeded {
137			p.raiseError(val, err)
138			return nil
139		}
140		if err != nil {
141			p.raiseErrorf(val, "Error parsing Include directive: %v", err)
142			return nil
143		}
144		lastHost.Nodes = append(lastHost.Nodes, inc)
145		return p.parseStart
146	}
147	kv := &KV{
148		Key:          key.val,
149		Value:        val.val,
150		Comment:      comment,
151		hasEquals:    hasEquals,
152		leadingSpace: key.Position.Col - 1,
153		position:     key.Position,
154	}
155	lastHost.Nodes = append(lastHost.Nodes, kv)
156	return p.parseStart
157}
158
159func (p *sshParser) parseComment() sshParserStateFn {
160	comment := p.getToken()
161	lastHost := p.config.Hosts[len(p.config.Hosts)-1]
162	lastHost.Nodes = append(lastHost.Nodes, &Empty{
163		Comment: comment.val,
164		// account for the "#" as well
165		leadingSpace: comment.Position.Col - 2,
166		position:     comment.Position,
167	})
168	return p.parseStart
169}
170
171func parseSSH(flow chan token, system bool, depth uint8) *Config {
172	// Ensure we consume tokens to completion even if parser exits early
173	defer func() {
174		for range flow {
175		}
176	}()
177
178	result := newConfig()
179	result.position = Position{1, 1}
180	parser := &sshParser{
181		flow:          flow,
182		config:        result,
183		tokensBuffer:  make([]token, 0),
184		currentTable:  make([]string, 0),
185		seenTableKeys: make([]string, 0),
186		system:        system,
187		depth:         depth,
188	}
189	parser.run()
190	return result
191}
192