PageRenderTime 60ms CodeModel.GetById 45ms app.highlight 11ms RepoModel.GetById 1ms app.codeStats 0ms

/netrc/netrc.go

https://code.google.com/p/go-netrc/
Go | 259 lines | 208 code | 23 blank | 28 comment | 57 complexity | 1acad116572896e43ec3b2d912ee4445 MD5 | raw file
  1// Copyright Š 2010 Fazlul Shahriar <fshahriar@gmail.com>.
  2// See LICENSE file for license details.
  3
  4// Package netrc implements a parser for netrc file format.
  5//
  6// A netrc file usually resides in $HOME/.netrc and is traditionally used
  7// by the ftp(1) program to look up login information (username, password,
  8// etc.) of remote system(s). The file format is (loosely) described in
  9// this man page: http://linux.die.net/man/5/netrc .
 10package netrc
 11
 12import (
 13	"bytes"
 14	"errors"
 15	"fmt"
 16	"io"
 17	"io/ioutil"
 18	"os"
 19	"unicode"
 20	"unicode/utf8"
 21)
 22
 23const (
 24	tkMachine = iota
 25	tkDefault
 26	tkLogin
 27	tkPassword
 28	tkAccount
 29	tkMacdef
 30)
 31
 32var tokenNames = []string{
 33	"Machine",
 34	"Default",
 35	"Login",
 36	"Password",
 37	"Account",
 38	"Macdef",
 39}
 40
 41var keywords = map[string]int{
 42	"machine":  tkMachine,
 43	"default":  tkDefault,
 44	"login":    tkLogin,
 45	"password": tkPassword,
 46	"account":  tkAccount,
 47	"macdef":   tkMacdef,
 48}
 49
 50// Machine contains information about a remote machine.
 51type Machine struct {
 52	Name     string
 53	Login    string
 54	Password string
 55	Account  string
 56}
 57
 58// Macros contains all the macro definitions in a netrc file.
 59type Macros map[string]string
 60
 61type token struct {
 62	kind      int
 63	macroName string
 64	value     string
 65}
 66
 67type filePos struct {
 68	name string
 69	line int
 70}
 71
 72// Error represents a netrc file parse error.
 73type Error struct {
 74	Filename string
 75	LineNum  int    // Line number
 76	Msg      string // Error message
 77}
 78
 79// Error returns a string representation of error e.
 80func (e *Error) Error() string {
 81	return fmt.Sprintf("%s:%d: %s", e.Filename, e.LineNum, e.Msg)
 82}
 83
 84func getWord(b []byte, pos *filePos) (string, []byte) {
 85	// Skip over leading whitespace
 86	i := 0
 87	for i < len(b) {
 88		r, size := utf8.DecodeRune(b[i:])
 89		if r == '\n' {
 90			pos.line++
 91		}
 92		if !unicode.IsSpace(r) {
 93			break
 94		}
 95		i += size
 96	}
 97	b = b[i:]
 98
 99	// Find end of word
100	i = bytes.IndexFunc(b, unicode.IsSpace)
101	if i < 0 {
102		i = len(b)
103	}
104	return string(b[0:i]), b[i:]
105}
106
107func getToken(b []byte, pos *filePos) ([]byte, *token, error) {
108	word, b := getWord(b, pos)
109	if word == "" {
110		return b, nil, nil // EOF reached
111	}
112
113	t := new(token)
114	var ok bool
115	t.kind, ok = keywords[word]
116	if !ok {
117		return b, nil, &Error{pos.name, pos.line, "keyword expected; got " + word}
118	}
119	if t.kind == tkDefault {
120		return b, t, nil
121	}
122
123	word, b = getWord(b, pos)
124	if word == "" {
125		return b, nil, &Error{pos.name, pos.line, "word expected"}
126	}
127	if t.kind == tkMacdef {
128		t.macroName = word
129
130		// Macro value starts on next line. The rest of current line
131		// should contain nothing but whitespace
132		i := 0
133		for i < len(b) {
134			r, size := utf8.DecodeRune(b[i:])
135			if r == '\n' {
136				i += size
137				pos.line++
138				break
139			}
140			if !unicode.IsSpace(r) {
141				return b, nil, &Error{pos.name, pos.line, "unexpected word"}
142			}
143			i += size
144		}
145		b = b[i:]
146
147		// Find end of macro value
148		i = bytes.Index(b, []byte("\n\n"))
149		if i < 0 { // EOF reached
150			i = len(b)
151		}
152		t.value = string(b[0:i])
153
154		return b[i:], t, nil
155	}
156	t.value = word
157	return b, t, nil
158}
159
160func parse(r io.Reader, pos *filePos) ([]*Machine, Macros, error) {
161	// TODO(fhs): Clear memory containing password.
162	b, err := ioutil.ReadAll(r)
163	if err != nil {
164		return nil, nil, err
165	}
166
167	mach := make([]*Machine, 0, 20)
168	mac := make(Macros, 10)
169	var defaultSeen bool
170	var m *Machine
171	var t *token
172	for {
173		b, t, err = getToken(b, pos)
174		if err != nil {
175			return nil, nil, err
176		}
177		if t == nil {
178			break
179		}
180		switch t.kind {
181		case tkMacdef:
182			mac[t.macroName] = t.value
183		case tkDefault:
184			if defaultSeen {
185				return nil, nil, &Error{pos.name, pos.line, "multiple default token"}
186			}
187			if m != nil {
188				mach, m = append(mach, m), nil
189			}
190			m = new(Machine)
191			m.Name = ""
192			defaultSeen = true
193		case tkMachine:
194			if m != nil {
195				mach, m = append(mach, m), nil
196			}
197			m = new(Machine)
198			m.Name = t.value
199		case tkLogin:
200			if m == nil || m.Login != "" {
201				return nil, nil, &Error{pos.name, pos.line, "unexpected token login "}
202			}
203			m.Login = t.value
204		case tkPassword:
205			if m == nil || m.Password != "" {
206				return nil, nil, &Error{pos.name, pos.line, "unexpected token password"}
207			}
208			m.Password = t.value
209		case tkAccount:
210			if m == nil || m.Account != "" {
211				return nil, nil, &Error{pos.name, pos.line, "unexpected token account"}
212			}
213			m.Account = t.value
214		}
215	}
216	if m != nil {
217		mach, m = append(mach, m), nil
218	}
219	return mach, mac, nil
220}
221
222// ParseFile parses the netrc file identified by filename and returns the set of
223// machine information and macros defined in it. The ``default'' machine,
224// which is intended to be used when no machine name matches, is identified
225// by an empty machine name. There can be only one ``default'' machine.
226//
227// If there is a parsing error, an Error is returned.
228func ParseFile(filename string) ([]*Machine, Macros, error) {
229	// TODO(fhs): Check if file is readable by anyone besides the user if there is password in it.
230	fd, err := os.Open(filename)
231	if err != nil {
232		return nil, nil, err
233	}
234	defer fd.Close()
235	return parse(fd, &filePos{filename, 1})
236}
237
238// FindMachine parses the netrc file identified by filename and returns
239// the Machine named by name. If no Machine with name name is found, the
240// ``default'' machine is returned.
241func FindMachine(filename, name string) (*Machine, error) {
242	mach, _, err := ParseFile(filename)
243	if err != nil {
244		return nil, err
245	}
246	var def *Machine
247	for _, m := range mach {
248		if m.Name == name {
249			return m, nil
250		}
251		if m.Name == "" {
252			def = m
253		}
254	}
255	if def == nil {
256		return nil, errors.New("no machine found")
257	}
258	return def, nil
259}