blob: ea49987c081d7014b068808d7399a7fb2e658391 [file] [log] [blame]
package netrc
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"sync"
"unicode"
"unicode/utf8"
)
type tkType int
const (
tkMachine tkType = iota
tkDefault
tkLogin
tkPassword
tkAccount
tkMacdef
tkComment
tkWhitespace
)
var keywords = map[string]tkType{
"machine": tkMachine,
"default": tkDefault,
"login": tkLogin,
"password": tkPassword,
"account": tkAccount,
"macdef": tkMacdef,
"#": tkComment,
}
type Netrc struct {
tokens []*token
machines []*Machine
macros Macros
updateLock sync.Mutex
}
// FindMachine returns the Machine in n named by name. If a machine named by
// name exists, it is returned. If no Machine with name name is found and there
// is a ``default'' machine, the ``default'' machine is returned. Otherwise, nil
// is returned.
func (n *Netrc) FindMachine(name string) (m *Machine) {
// TODO(bgentry): not safe for concurrency
var def *Machine
for _, m = range n.machines {
if m.Name == name {
return m
}
if m.IsDefault() {
def = m
}
}
if def == nil {
return nil
}
return def
}
// MarshalText implements the encoding.TextMarshaler interface to encode a
// Netrc into text format.
func (n *Netrc) MarshalText() (text []byte, err error) {
// TODO(bgentry): not safe for concurrency
for i := range n.tokens {
switch n.tokens[i].kind {
case tkComment, tkDefault, tkWhitespace: // always append these types
text = append(text, n.tokens[i].rawkind...)
default:
if n.tokens[i].value != "" { // skip empty-value tokens
text = append(text, n.tokens[i].rawkind...)
}
}
if n.tokens[i].kind == tkMacdef {
text = append(text, ' ')
text = append(text, n.tokens[i].macroName...)
}
text = append(text, n.tokens[i].rawvalue...)
}
return
}
func (n *Netrc) NewMachine(name, login, password, account string) *Machine {
n.updateLock.Lock()
defer n.updateLock.Unlock()
prefix := "\n"
if len(n.tokens) == 0 {
prefix = ""
}
m := &Machine{
Name: name,
Login: login,
Password: password,
Account: account,
nametoken: &token{
kind: tkMachine,
rawkind: []byte(prefix + "machine"),
value: name,
rawvalue: []byte(" " + name),
},
logintoken: &token{
kind: tkLogin,
rawkind: []byte("\n\tlogin"),
value: login,
rawvalue: []byte(" " + login),
},
passtoken: &token{
kind: tkPassword,
rawkind: []byte("\n\tpassword"),
value: password,
rawvalue: []byte(" " + password),
},
accounttoken: &token{
kind: tkAccount,
rawkind: []byte("\n\taccount"),
value: account,
rawvalue: []byte(" " + account),
},
}
n.insertMachineTokensBeforeDefault(m)
for i := range n.machines {
if n.machines[i].IsDefault() {
n.machines = append(append(n.machines[:i], m), n.machines[i:]...)
return m
}
}
n.machines = append(n.machines, m)
return m
}
func (n *Netrc) insertMachineTokensBeforeDefault(m *Machine) {
newtokens := []*token{m.nametoken}
if m.logintoken.value != "" {
newtokens = append(newtokens, m.logintoken)
}
if m.passtoken.value != "" {
newtokens = append(newtokens, m.passtoken)
}
if m.accounttoken.value != "" {
newtokens = append(newtokens, m.accounttoken)
}
for i := range n.tokens {
if n.tokens[i].kind == tkDefault {
// found the default, now insert tokens before it
n.tokens = append(n.tokens[:i], append(newtokens, n.tokens[i:]...)...)
return
}
}
// didn't find a default, just add the newtokens to the end
n.tokens = append(n.tokens, newtokens...)
return
}
func (n *Netrc) RemoveMachine(name string) {
n.updateLock.Lock()
defer n.updateLock.Unlock()
for i := range n.machines {
if n.machines[i] != nil && n.machines[i].Name == name {
m := n.machines[i]
for _, t := range []*token{
m.nametoken, m.logintoken, m.passtoken, m.accounttoken,
} {
n.removeToken(t)
}
n.machines = append(n.machines[:i], n.machines[i+1:]...)
return
}
}
}
func (n *Netrc) removeToken(t *token) {
if t != nil {
for i := range n.tokens {
if n.tokens[i] == t {
n.tokens = append(n.tokens[:i], n.tokens[i+1:]...)
return
}
}
}
}
// Machine contains information about a remote machine.
type Machine struct {
Name string
Login string
Password string
Account string
nametoken *token
logintoken *token
passtoken *token
accounttoken *token
}
// IsDefault returns true if the machine is a "default" token, denoted by an
// empty name.
func (m *Machine) IsDefault() bool {
return m.Name == ""
}
// UpdatePassword sets the password for the Machine m.
func (m *Machine) UpdatePassword(newpass string) {
m.Password = newpass
updateTokenValue(m.passtoken, newpass)
}
// UpdateLogin sets the login for the Machine m.
func (m *Machine) UpdateLogin(newlogin string) {
m.Login = newlogin
updateTokenValue(m.logintoken, newlogin)
}
// UpdateAccount sets the login for the Machine m.
func (m *Machine) UpdateAccount(newaccount string) {
m.Account = newaccount
updateTokenValue(m.accounttoken, newaccount)
}
func updateTokenValue(t *token, value string) {
oldvalue := t.value
t.value = value
newraw := make([]byte, len(t.rawvalue))
copy(newraw, t.rawvalue)
t.rawvalue = append(
bytes.TrimSuffix(newraw, []byte(oldvalue)),
[]byte(value)...,
)
}
// Macros contains all the macro definitions in a netrc file.
type Macros map[string]string
type token struct {
kind tkType
macroName string
value string
rawkind []byte
rawvalue []byte
}
// Error represents a netrc file parse error.
type Error struct {
LineNum int // Line number
Msg string // Error message
}
// Error returns a string representation of error e.
func (e *Error) Error() string {
return fmt.Sprintf("line %d: %s", e.LineNum, e.Msg)
}
func (e *Error) BadDefaultOrder() bool {
return e.Msg == errBadDefaultOrder
}
const errBadDefaultOrder = "default token must appear after all machine tokens"
// scanLinesKeepPrefix is a split function for a Scanner that returns each line
// of text. The returned token may include newlines if they are before the
// first non-space character. The returned line may be empty. The end-of-line
// marker is one optional carriage return followed by one mandatory newline. In
// regular expression notation, it is `\r?\n`. The last non-empty line of
// input will be returned even if it has no newline.
func scanLinesKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
// Skip leading spaces.
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !unicode.IsSpace(r) {
break
}
}
if i := bytes.IndexByte(data[start:], '\n'); i >= 0 {
// We have a full newline-terminated line.
return start + i, data[0 : start+i], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
// scanWordsKeepPrefix is a split function for a Scanner that returns each
// space-separated word of text, with prefixing spaces included. It will never
// return an empty string. The definition of space is set by unicode.IsSpace.
//
// Adapted from bufio.ScanWords().
func scanTokensKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) {
// Skip leading spaces.
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !unicode.IsSpace(r) {
break
}
}
if atEOF && len(data) == 0 || start == len(data) {
return len(data), data, nil
}
if len(data) > start && data[start] == '#' {
return scanLinesKeepPrefix(data, atEOF)
}
// Scan until space, marking end of word.
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if unicode.IsSpace(r) {
return i, data[:i], nil
}
}
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
if atEOF && len(data) > start {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
func newToken(rawb []byte) (*token, error) {
_, tkind, err := bufio.ScanWords(rawb, true)
if err != nil {
return nil, err
}
var ok bool
t := token{rawkind: rawb}
t.kind, ok = keywords[string(tkind)]
if !ok {
trimmed := strings.TrimSpace(string(tkind))
if trimmed == "" {
t.kind = tkWhitespace // whitespace-only, should happen only at EOF
return &t, nil
}
if strings.HasPrefix(trimmed, "#") {
t.kind = tkComment // this is a comment
return &t, nil
}
return &t, fmt.Errorf("keyword expected; got " + string(tkind))
}
return &t, nil
}
func scanValue(scanner *bufio.Scanner, pos int) ([]byte, string, int, error) {
if scanner.Scan() {
raw := scanner.Bytes()
pos += bytes.Count(raw, []byte{'\n'})
return raw, strings.TrimSpace(string(raw)), pos, nil
}
if err := scanner.Err(); err != nil {
return nil, "", pos, &Error{pos, err.Error()}
}
return nil, "", pos, nil
}
func parse(r io.Reader, pos int) (*Netrc, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
nrc := Netrc{machines: make([]*Machine, 0, 20), macros: make(Macros, 10)}
defaultSeen := false
var currentMacro *token
var m *Machine
var t *token
scanner := bufio.NewScanner(bytes.NewReader(b))
scanner.Split(scanTokensKeepPrefix)
for scanner.Scan() {
rawb := scanner.Bytes()
if len(rawb) == 0 {
break
}
pos += bytes.Count(rawb, []byte{'\n'})
t, err = newToken(rawb)
if err != nil {
if currentMacro == nil {
return nil, &Error{pos, err.Error()}
}
currentMacro.rawvalue = append(currentMacro.rawvalue, rawb...)
continue
}
if currentMacro != nil && bytes.Contains(rawb, []byte{'\n', '\n'}) {
// if macro rawvalue + rawb would contain \n\n, then macro def is over
currentMacro.value = strings.TrimLeft(string(currentMacro.rawvalue), "\r\n")
nrc.macros[currentMacro.macroName] = currentMacro.value
currentMacro = nil
}
switch t.kind {
case tkMacdef:
if _, t.macroName, pos, err = scanValue(scanner, pos); err != nil {
return nil, &Error{pos, err.Error()}
}
currentMacro = t
case tkDefault:
if defaultSeen {
return nil, &Error{pos, "multiple default token"}
}
if m != nil {
nrc.machines, m = append(nrc.machines, m), nil
}
m = new(Machine)
m.Name = ""
defaultSeen = true
case tkMachine:
if defaultSeen {
return nil, &Error{pos, errBadDefaultOrder}
}
if m != nil {
nrc.machines, m = append(nrc.machines, m), nil
}
m = new(Machine)
if t.rawvalue, m.Name, pos, err = scanValue(scanner, pos); err != nil {
return nil, &Error{pos, err.Error()}
}
t.value = m.Name
m.nametoken = t
case tkLogin:
if m == nil || m.Login != "" {
return nil, &Error{pos, "unexpected token login "}
}
if t.rawvalue, m.Login, pos, err = scanValue(scanner, pos); err != nil {
return nil, &Error{pos, err.Error()}
}
t.value = m.Login
m.logintoken = t
case tkPassword:
if m == nil || m.Password != "" {
return nil, &Error{pos, "unexpected token password"}
}
if t.rawvalue, m.Password, pos, err = scanValue(scanner, pos); err != nil {
return nil, &Error{pos, err.Error()}
}
t.value = m.Password
m.passtoken = t
case tkAccount:
if m == nil || m.Account != "" {
return nil, &Error{pos, "unexpected token account"}
}
if t.rawvalue, m.Account, pos, err = scanValue(scanner, pos); err != nil {
return nil, &Error{pos, err.Error()}
}
t.value = m.Account
m.accounttoken = t
}
nrc.tokens = append(nrc.tokens, t)
}
if err := scanner.Err(); err != nil {
return nil, err
}
if m != nil {
nrc.machines, m = append(nrc.machines, m), nil
}
return &nrc, nil
}
// ParseFile opens the file at filename and then passes its io.Reader to
// Parse().
func ParseFile(filename string) (*Netrc, error) {
fd, err := os.Open(filename)
if err != nil {
return nil, err
}
defer fd.Close()
return Parse(fd)
}
// Parse parses from the the Reader r as a netrc file and returns the set of
// machine information and macros defined in it. The ``default'' machine,
// which is intended to be used when no machine name matches, is identified
// by an empty machine name. There can be only one ``default'' machine.
//
// If there is a parsing error, an Error is returned.
func Parse(r io.Reader) (*Netrc, error) {
return parse(r, 1)
}
// FindMachine parses the netrc file identified by filename and returns the
// Machine named by name. If a problem occurs parsing the file at filename, an
// error is returned. If a machine named by name exists, it is returned. If no
// Machine with name name is found and there is a ``default'' machine, the
// ``default'' machine is returned. Otherwise, nil is returned.
func FindMachine(filename, name string) (m *Machine, err error) {
n, err := ParseFile(filename)
if err != nil {
return nil, err
}
return n.FindMachine(name), nil
}