├── .hgignore ├── LICENSE ├── README.md └── netrc ├── examples ├── bad_default_order.netrc └── good.netrc ├── netrc.go └── netrc_test.go /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.8 3 | *.a 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Original version Copyright © 2010 Fazlul Shahriar . Newer 2 | portions Copyright © 2014 Blake Gentry . 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-netrc 2 | 3 | A Golang package for reading and writing netrc files. This package can parse netrc 4 | files, make changes to them, and then serialize them back to netrc format, while 5 | preserving any whitespace that was present in the source file. 6 | 7 | [![GoDoc](https://godoc.org/github.com/bgentry/go-netrc?status.png)][godoc] 8 | 9 | [godoc]: https://godoc.org/github.com/bgentry/go-netrc "go-netrc on Godoc.org" 10 | -------------------------------------------------------------------------------- /netrc/examples/bad_default_order.netrc: -------------------------------------------------------------------------------- 1 | # I am a comment 2 | machine mail.google.com 3 | login joe@gmail.com 4 | account gmail 5 | password somethingSecret 6 | # I am another comment 7 | 8 | default 9 | login anonymous 10 | password joe@example.com 11 | 12 | machine ray login demo password mypassword 13 | 14 | -------------------------------------------------------------------------------- /netrc/examples/good.netrc: -------------------------------------------------------------------------------- 1 | # I am a comment 2 | machine mail.google.com 3 | login joe@gmail.com 4 | account justagmail #end of line comment with trailing space 5 | password somethingSecret 6 | # I am another comment 7 | 8 | macdef allput 9 | put src/* 10 | 11 | macdef allput2 12 | put src/* 13 | put src2/* 14 | 15 | machine ray login demo password mypassword 16 | 17 | machine weirdlogin login uname password pass#pass 18 | 19 | default 20 | login anonymous 21 | password joe@example.com 22 | 23 | -------------------------------------------------------------------------------- /netrc/netrc.go: -------------------------------------------------------------------------------- 1 | package netrc 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "strings" 11 | "sync" 12 | "unicode" 13 | "unicode/utf8" 14 | ) 15 | 16 | type tkType int 17 | 18 | const ( 19 | tkMachine tkType = iota 20 | tkDefault 21 | tkLogin 22 | tkPassword 23 | tkAccount 24 | tkMacdef 25 | tkComment 26 | tkWhitespace 27 | ) 28 | 29 | var keywords = map[string]tkType{ 30 | "machine": tkMachine, 31 | "default": tkDefault, 32 | "login": tkLogin, 33 | "password": tkPassword, 34 | "account": tkAccount, 35 | "macdef": tkMacdef, 36 | "#": tkComment, 37 | } 38 | 39 | type Netrc struct { 40 | tokens []*token 41 | machines []*Machine 42 | macros Macros 43 | updateLock sync.Mutex 44 | } 45 | 46 | // FindMachine returns the Machine in n named by name. If a machine named by 47 | // name exists, it is returned. If no Machine with name name is found and there 48 | // is a ``default'' machine, the ``default'' machine is returned. Otherwise, nil 49 | // is returned. 50 | func (n *Netrc) FindMachine(name string) (m *Machine) { 51 | // TODO(bgentry): not safe for concurrency 52 | var def *Machine 53 | for _, m = range n.machines { 54 | if m.Name == name { 55 | return m 56 | } 57 | if m.IsDefault() { 58 | def = m 59 | } 60 | } 61 | if def == nil { 62 | return nil 63 | } 64 | return def 65 | } 66 | 67 | // MarshalText implements the encoding.TextMarshaler interface to encode a 68 | // Netrc into text format. 69 | func (n *Netrc) MarshalText() (text []byte, err error) { 70 | // TODO(bgentry): not safe for concurrency 71 | for i := range n.tokens { 72 | switch n.tokens[i].kind { 73 | case tkComment, tkDefault, tkWhitespace: // always append these types 74 | text = append(text, n.tokens[i].rawkind...) 75 | default: 76 | if n.tokens[i].value != "" { // skip empty-value tokens 77 | text = append(text, n.tokens[i].rawkind...) 78 | } 79 | } 80 | if n.tokens[i].kind == tkMacdef { 81 | text = append(text, ' ') 82 | text = append(text, n.tokens[i].macroName...) 83 | } 84 | text = append(text, n.tokens[i].rawvalue...) 85 | } 86 | return 87 | } 88 | 89 | func (n *Netrc) NewMachine(name, login, password, account string) *Machine { 90 | n.updateLock.Lock() 91 | defer n.updateLock.Unlock() 92 | 93 | prefix := "\n" 94 | if len(n.tokens) == 0 { 95 | prefix = "" 96 | } 97 | m := &Machine{ 98 | Name: name, 99 | Login: login, 100 | Password: password, 101 | Account: account, 102 | 103 | nametoken: &token{ 104 | kind: tkMachine, 105 | rawkind: []byte(prefix + "machine"), 106 | value: name, 107 | rawvalue: []byte(" " + name), 108 | }, 109 | logintoken: &token{ 110 | kind: tkLogin, 111 | rawkind: []byte("\n\tlogin"), 112 | value: login, 113 | rawvalue: []byte(" " + login), 114 | }, 115 | passtoken: &token{ 116 | kind: tkPassword, 117 | rawkind: []byte("\n\tpassword"), 118 | value: password, 119 | rawvalue: []byte(" " + password), 120 | }, 121 | accounttoken: &token{ 122 | kind: tkAccount, 123 | rawkind: []byte("\n\taccount"), 124 | value: account, 125 | rawvalue: []byte(" " + account), 126 | }, 127 | } 128 | n.insertMachineTokensBeforeDefault(m) 129 | for i := range n.machines { 130 | if n.machines[i].IsDefault() { 131 | n.machines = append(append(n.machines[:i], m), n.machines[i:]...) 132 | return m 133 | } 134 | } 135 | n.machines = append(n.machines, m) 136 | return m 137 | } 138 | 139 | func (n *Netrc) insertMachineTokensBeforeDefault(m *Machine) { 140 | newtokens := []*token{m.nametoken} 141 | if m.logintoken.value != "" { 142 | newtokens = append(newtokens, m.logintoken) 143 | } 144 | if m.passtoken.value != "" { 145 | newtokens = append(newtokens, m.passtoken) 146 | } 147 | if m.accounttoken.value != "" { 148 | newtokens = append(newtokens, m.accounttoken) 149 | } 150 | for i := range n.tokens { 151 | if n.tokens[i].kind == tkDefault { 152 | // found the default, now insert tokens before it 153 | n.tokens = append(n.tokens[:i], append(newtokens, n.tokens[i:]...)...) 154 | return 155 | } 156 | } 157 | // didn't find a default, just add the newtokens to the end 158 | n.tokens = append(n.tokens, newtokens...) 159 | return 160 | } 161 | 162 | func (n *Netrc) RemoveMachine(name string) { 163 | n.updateLock.Lock() 164 | defer n.updateLock.Unlock() 165 | 166 | for i := range n.machines { 167 | if n.machines[i] != nil && n.machines[i].Name == name { 168 | m := n.machines[i] 169 | for _, t := range []*token{ 170 | m.nametoken, m.logintoken, m.passtoken, m.accounttoken, 171 | } { 172 | n.removeToken(t) 173 | } 174 | n.machines = append(n.machines[:i], n.machines[i+1:]...) 175 | return 176 | } 177 | } 178 | } 179 | 180 | func (n *Netrc) removeToken(t *token) { 181 | if t != nil { 182 | for i := range n.tokens { 183 | if n.tokens[i] == t { 184 | n.tokens = append(n.tokens[:i], n.tokens[i+1:]...) 185 | return 186 | } 187 | } 188 | } 189 | } 190 | 191 | // Machine contains information about a remote machine. 192 | type Machine struct { 193 | Name string 194 | Login string 195 | Password string 196 | Account string 197 | 198 | nametoken *token 199 | logintoken *token 200 | passtoken *token 201 | accounttoken *token 202 | } 203 | 204 | // IsDefault returns true if the machine is a "default" token, denoted by an 205 | // empty name. 206 | func (m *Machine) IsDefault() bool { 207 | return m.Name == "" 208 | } 209 | 210 | // UpdatePassword sets the password for the Machine m. 211 | func (m *Machine) UpdatePassword(newpass string) { 212 | m.Password = newpass 213 | updateTokenValue(m.passtoken, newpass) 214 | } 215 | 216 | // UpdateLogin sets the login for the Machine m. 217 | func (m *Machine) UpdateLogin(newlogin string) { 218 | m.Login = newlogin 219 | updateTokenValue(m.logintoken, newlogin) 220 | } 221 | 222 | // UpdateAccount sets the login for the Machine m. 223 | func (m *Machine) UpdateAccount(newaccount string) { 224 | m.Account = newaccount 225 | updateTokenValue(m.accounttoken, newaccount) 226 | } 227 | 228 | func updateTokenValue(t *token, value string) { 229 | oldvalue := t.value 230 | t.value = value 231 | newraw := make([]byte, len(t.rawvalue)) 232 | copy(newraw, t.rawvalue) 233 | t.rawvalue = append( 234 | bytes.TrimSuffix(newraw, []byte(oldvalue)), 235 | []byte(value)..., 236 | ) 237 | } 238 | 239 | // Macros contains all the macro definitions in a netrc file. 240 | type Macros map[string]string 241 | 242 | type token struct { 243 | kind tkType 244 | macroName string 245 | value string 246 | rawkind []byte 247 | rawvalue []byte 248 | } 249 | 250 | // Error represents a netrc file parse error. 251 | type Error struct { 252 | LineNum int // Line number 253 | Msg string // Error message 254 | } 255 | 256 | // Error returns a string representation of error e. 257 | func (e *Error) Error() string { 258 | return fmt.Sprintf("line %d: %s", e.LineNum, e.Msg) 259 | } 260 | 261 | func (e *Error) BadDefaultOrder() bool { 262 | return e.Msg == errBadDefaultOrder 263 | } 264 | 265 | const errBadDefaultOrder = "default token must appear after all machine tokens" 266 | 267 | // scanLinesKeepPrefix is a split function for a Scanner that returns each line 268 | // of text. The returned token may include newlines if they are before the 269 | // first non-space character. The returned line may be empty. The end-of-line 270 | // marker is one optional carriage return followed by one mandatory newline. In 271 | // regular expression notation, it is `\r?\n`. The last non-empty line of 272 | // input will be returned even if it has no newline. 273 | func scanLinesKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { 274 | if atEOF && len(data) == 0 { 275 | return 0, nil, nil 276 | } 277 | // Skip leading spaces. 278 | start := 0 279 | for width := 0; start < len(data); start += width { 280 | var r rune 281 | r, width = utf8.DecodeRune(data[start:]) 282 | if !unicode.IsSpace(r) { 283 | break 284 | } 285 | } 286 | if i := bytes.IndexByte(data[start:], '\n'); i >= 0 { 287 | // We have a full newline-terminated line. 288 | return start + i, data[0 : start+i], nil 289 | } 290 | // If we're at EOF, we have a final, non-terminated line. Return it. 291 | if atEOF { 292 | return len(data), data, nil 293 | } 294 | // Request more data. 295 | return 0, nil, nil 296 | } 297 | 298 | // scanWordsKeepPrefix is a split function for a Scanner that returns each 299 | // space-separated word of text, with prefixing spaces included. It will never 300 | // return an empty string. The definition of space is set by unicode.IsSpace. 301 | // 302 | // Adapted from bufio.ScanWords(). 303 | func scanTokensKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) { 304 | // Skip leading spaces. 305 | start := 0 306 | for width := 0; start < len(data); start += width { 307 | var r rune 308 | r, width = utf8.DecodeRune(data[start:]) 309 | if !unicode.IsSpace(r) { 310 | break 311 | } 312 | } 313 | if atEOF && len(data) == 0 || start == len(data) { 314 | return len(data), data, nil 315 | } 316 | if len(data) > start && data[start] == '#' { 317 | return scanLinesKeepPrefix(data, atEOF) 318 | } 319 | // Scan until space, marking end of word. 320 | for width, i := 0, start; i < len(data); i += width { 321 | var r rune 322 | r, width = utf8.DecodeRune(data[i:]) 323 | if unicode.IsSpace(r) { 324 | return i, data[:i], nil 325 | } 326 | } 327 | // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. 328 | if atEOF && len(data) > start { 329 | return len(data), data, nil 330 | } 331 | // Request more data. 332 | return 0, nil, nil 333 | } 334 | 335 | func newToken(rawb []byte) (*token, error) { 336 | _, tkind, err := bufio.ScanWords(rawb, true) 337 | if err != nil { 338 | return nil, err 339 | } 340 | var ok bool 341 | t := token{rawkind: rawb} 342 | t.kind, ok = keywords[string(tkind)] 343 | if !ok { 344 | trimmed := strings.TrimSpace(string(tkind)) 345 | if trimmed == "" { 346 | t.kind = tkWhitespace // whitespace-only, should happen only at EOF 347 | return &t, nil 348 | } 349 | if strings.HasPrefix(trimmed, "#") { 350 | t.kind = tkComment // this is a comment 351 | return &t, nil 352 | } 353 | return &t, fmt.Errorf("keyword expected; got " + string(tkind)) 354 | } 355 | return &t, nil 356 | } 357 | 358 | func scanValue(scanner *bufio.Scanner, pos int) ([]byte, string, int, error) { 359 | if scanner.Scan() { 360 | raw := scanner.Bytes() 361 | pos += bytes.Count(raw, []byte{'\n'}) 362 | return raw, strings.TrimSpace(string(raw)), pos, nil 363 | } 364 | if err := scanner.Err(); err != nil { 365 | return nil, "", pos, &Error{pos, err.Error()} 366 | } 367 | return nil, "", pos, nil 368 | } 369 | 370 | func parse(r io.Reader, pos int) (*Netrc, error) { 371 | b, err := ioutil.ReadAll(r) 372 | if err != nil { 373 | return nil, err 374 | } 375 | 376 | nrc := Netrc{machines: make([]*Machine, 0, 20), macros: make(Macros, 10)} 377 | 378 | defaultSeen := false 379 | var currentMacro *token 380 | var m *Machine 381 | var t *token 382 | scanner := bufio.NewScanner(bytes.NewReader(b)) 383 | scanner.Split(scanTokensKeepPrefix) 384 | 385 | for scanner.Scan() { 386 | rawb := scanner.Bytes() 387 | if len(rawb) == 0 { 388 | break 389 | } 390 | pos += bytes.Count(rawb, []byte{'\n'}) 391 | t, err = newToken(rawb) 392 | if err != nil { 393 | if currentMacro == nil { 394 | return nil, &Error{pos, err.Error()} 395 | } 396 | currentMacro.rawvalue = append(currentMacro.rawvalue, rawb...) 397 | continue 398 | } 399 | 400 | if currentMacro != nil && bytes.Contains(rawb, []byte{'\n', '\n'}) { 401 | // if macro rawvalue + rawb would contain \n\n, then macro def is over 402 | currentMacro.value = strings.TrimLeft(string(currentMacro.rawvalue), "\r\n") 403 | nrc.macros[currentMacro.macroName] = currentMacro.value 404 | currentMacro = nil 405 | } 406 | 407 | switch t.kind { 408 | case tkMacdef: 409 | if _, t.macroName, pos, err = scanValue(scanner, pos); err != nil { 410 | return nil, &Error{pos, err.Error()} 411 | } 412 | currentMacro = t 413 | case tkDefault: 414 | if defaultSeen { 415 | return nil, &Error{pos, "multiple default token"} 416 | } 417 | if m != nil { 418 | nrc.machines, m = append(nrc.machines, m), nil 419 | } 420 | m = new(Machine) 421 | m.Name = "" 422 | defaultSeen = true 423 | case tkMachine: 424 | if defaultSeen { 425 | return nil, &Error{pos, errBadDefaultOrder} 426 | } 427 | if m != nil { 428 | nrc.machines, m = append(nrc.machines, m), nil 429 | } 430 | m = new(Machine) 431 | if t.rawvalue, m.Name, pos, err = scanValue(scanner, pos); err != nil { 432 | return nil, &Error{pos, err.Error()} 433 | } 434 | t.value = m.Name 435 | m.nametoken = t 436 | case tkLogin: 437 | if m == nil || m.Login != "" { 438 | return nil, &Error{pos, "unexpected token login "} 439 | } 440 | if t.rawvalue, m.Login, pos, err = scanValue(scanner, pos); err != nil { 441 | return nil, &Error{pos, err.Error()} 442 | } 443 | t.value = m.Login 444 | m.logintoken = t 445 | case tkPassword: 446 | if m == nil || m.Password != "" { 447 | return nil, &Error{pos, "unexpected token password"} 448 | } 449 | if t.rawvalue, m.Password, pos, err = scanValue(scanner, pos); err != nil { 450 | return nil, &Error{pos, err.Error()} 451 | } 452 | t.value = m.Password 453 | m.passtoken = t 454 | case tkAccount: 455 | if m == nil || m.Account != "" { 456 | return nil, &Error{pos, "unexpected token account"} 457 | } 458 | if t.rawvalue, m.Account, pos, err = scanValue(scanner, pos); err != nil { 459 | return nil, &Error{pos, err.Error()} 460 | } 461 | t.value = m.Account 462 | m.accounttoken = t 463 | } 464 | 465 | nrc.tokens = append(nrc.tokens, t) 466 | } 467 | 468 | if err := scanner.Err(); err != nil { 469 | return nil, err 470 | } 471 | 472 | if m != nil { 473 | nrc.machines, m = append(nrc.machines, m), nil 474 | } 475 | return &nrc, nil 476 | } 477 | 478 | // ParseFile opens the file at filename and then passes its io.Reader to 479 | // Parse(). 480 | func ParseFile(filename string) (*Netrc, error) { 481 | fd, err := os.Open(filename) 482 | if err != nil { 483 | return nil, err 484 | } 485 | defer fd.Close() 486 | return Parse(fd) 487 | } 488 | 489 | // Parse parses from the the Reader r as a netrc file and returns the set of 490 | // machine information and macros defined in it. The ``default'' machine, 491 | // which is intended to be used when no machine name matches, is identified 492 | // by an empty machine name. There can be only one ``default'' machine. 493 | // 494 | // If there is a parsing error, an Error is returned. 495 | func Parse(r io.Reader) (*Netrc, error) { 496 | return parse(r, 1) 497 | } 498 | 499 | // FindMachine parses the netrc file identified by filename and returns the 500 | // Machine named by name. If a problem occurs parsing the file at filename, an 501 | // error is returned. If a machine named by name exists, it is returned. If no 502 | // Machine with name name is found and there is a ``default'' machine, the 503 | // ``default'' machine is returned. Otherwise, nil is returned. 504 | func FindMachine(filename, name string) (m *Machine, err error) { 505 | n, err := ParseFile(filename) 506 | if err != nil { 507 | return nil, err 508 | } 509 | return n.FindMachine(name), nil 510 | } 511 | -------------------------------------------------------------------------------- /netrc/netrc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2010 Fazlul Shahriar and 2 | // Copyright © 2014 Blake Gentry . 3 | // See LICENSE file for license details. 4 | 5 | package netrc 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "os" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | var expectedMachines = []*Machine{ 18 | &Machine{Name: "mail.google.com", Login: "joe@gmail.com", Password: "somethingSecret", Account: "justagmail"}, 19 | &Machine{Name: "ray", Login: "demo", Password: "mypassword", Account: ""}, 20 | &Machine{Name: "weirdlogin", Login: "uname", Password: "pass#pass", Account: ""}, 21 | &Machine{Name: "", Login: "anonymous", Password: "joe@example.com", Account: ""}, 22 | } 23 | var expectedMacros = Macros{ 24 | "allput": "put src/*", 25 | "allput2": " put src/*\nput src2/*", 26 | } 27 | 28 | func eqMachine(a *Machine, b *Machine) bool { 29 | return a.Name == b.Name && 30 | a.Login == b.Login && 31 | a.Password == b.Password && 32 | a.Account == b.Account 33 | } 34 | 35 | func testExpected(n *Netrc, t *testing.T) { 36 | if len(expectedMachines) != len(n.machines) { 37 | t.Errorf("expected %d machines, got %d", len(expectedMachines), len(n.machines)) 38 | } else { 39 | for i, e := range expectedMachines { 40 | if !eqMachine(e, n.machines[i]) { 41 | t.Errorf("bad machine; expected %v, got %v\n", e, n.machines[i]) 42 | } 43 | } 44 | } 45 | 46 | if len(expectedMacros) != len(n.macros) { 47 | t.Errorf("expected %d macros, got %d", len(expectedMacros), len(n.macros)) 48 | } else { 49 | for k, v := range expectedMacros { 50 | if v != n.macros[k] { 51 | t.Errorf("bad macro for %s; expected %q, got %q\n", k, v, n.macros[k]) 52 | } 53 | } 54 | } 55 | } 56 | 57 | var newTokenTests = []struct { 58 | rawkind string 59 | tkind tkType 60 | }{ 61 | {"machine", tkMachine}, 62 | {"\n\n\tmachine", tkMachine}, 63 | {"\n machine", tkMachine}, 64 | {"default", tkDefault}, 65 | {"login", tkLogin}, 66 | {"password", tkPassword}, 67 | {"account", tkAccount}, 68 | {"macdef", tkMacdef}, 69 | {"\n # comment stuff ", tkComment}, 70 | {"\n # I am another comment", tkComment}, 71 | {"\n\t\n ", tkWhitespace}, 72 | } 73 | 74 | var newTokenInvalidTests = []string{ 75 | " junk", 76 | "sdfdsf", 77 | "account#unspaced comment", 78 | } 79 | 80 | func TestNewToken(t *testing.T) { 81 | for _, tktest := range newTokenTests { 82 | tok, err := newToken([]byte(tktest.rawkind)) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | if tok.kind != tktest.tkind { 87 | t.Errorf("expected tok.kind %d, got %d", tktest.tkind, tok.kind) 88 | } 89 | if string(tok.rawkind) != tktest.rawkind { 90 | t.Errorf("expected tok.rawkind %q, got %q", tktest.rawkind, string(tok.rawkind)) 91 | } 92 | } 93 | 94 | for _, tktest := range newTokenInvalidTests { 95 | _, err := newToken([]byte(tktest)) 96 | if err == nil { 97 | t.Errorf("expected error with %q, got none", tktest) 98 | } 99 | } 100 | } 101 | 102 | func TestParse(t *testing.T) { 103 | r := netrcReader("examples/good.netrc", t) 104 | n, err := Parse(r) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | testExpected(n, t) 109 | } 110 | 111 | func TestParseFile(t *testing.T) { 112 | n, err := ParseFile("examples/good.netrc") 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | testExpected(n, t) 117 | 118 | _, err = ParseFile("examples/bad_default_order.netrc") 119 | if err == nil { 120 | t.Error("expected an error parsing bad_default_order.netrc, got none") 121 | } else if !err.(*Error).BadDefaultOrder() { 122 | t.Error("expected BadDefaultOrder() to be true, got false") 123 | } 124 | 125 | _, err = ParseFile("examples/this_file_doesnt_exist.netrc") 126 | if err == nil { 127 | t.Error("expected an error loading this_file_doesnt_exist.netrc, got none") 128 | } else if _, ok := err.(*os.PathError); !ok { 129 | t.Errorf("expected *os.Error, got %v", err) 130 | } 131 | } 132 | 133 | func TestFindMachine(t *testing.T) { 134 | m, err := FindMachine("examples/good.netrc", "ray") 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | if !eqMachine(m, expectedMachines[1]) { 139 | t.Errorf("bad machine; expected %v, got %v\n", expectedMachines[1], m) 140 | } 141 | if m.IsDefault() { 142 | t.Errorf("expected m.IsDefault() to be false") 143 | } 144 | 145 | m, err = FindMachine("examples/good.netrc", "non.existent") 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | if !eqMachine(m, expectedMachines[3]) { 150 | t.Errorf("bad machine; expected %v, got %v\n", expectedMachines[3], m) 151 | } 152 | if !m.IsDefault() { 153 | t.Errorf("expected m.IsDefault() to be true") 154 | } 155 | } 156 | 157 | func TestNetrcFindMachine(t *testing.T) { 158 | n, err := ParseFile("examples/good.netrc") 159 | if err != nil { 160 | t.Fatal(err) 161 | } 162 | 163 | m := n.FindMachine("ray") 164 | if !eqMachine(m, expectedMachines[1]) { 165 | t.Errorf("bad machine; expected %v, got %v\n", expectedMachines[1], m) 166 | } 167 | if m.IsDefault() { 168 | t.Errorf("expected def to be false") 169 | } 170 | 171 | n = &Netrc{} 172 | m = n.FindMachine("nonexistent") 173 | if m != nil { 174 | t.Errorf("expected nil, got %v", m) 175 | } 176 | } 177 | 178 | func TestMarshalText(t *testing.T) { 179 | // load up expected netrc Marshal output 180 | expected, err := ioutil.ReadAll(netrcReader("examples/good.netrc", t)) 181 | if err != nil { 182 | t.Fatal(err) 183 | } 184 | 185 | n, err := ParseFile("examples/good.netrc") 186 | if err != nil { 187 | t.Fatal(err) 188 | } 189 | 190 | result, err := n.MarshalText() 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | if string(result) != string(expected) { 195 | t.Errorf("expected:\n%q\ngot:\n%q", string(expected), string(result)) 196 | } 197 | 198 | // make sure tokens w/ no value are not serialized 199 | m := n.FindMachine("mail.google.com") 200 | m.UpdatePassword("") 201 | result, err = n.MarshalText() 202 | if err != nil { 203 | t.Fatal(err) 204 | } 205 | if strings.Contains(string(result), "\tpassword \n") { 206 | fmt.Println(string(result)) 207 | t.Errorf("expected zero-value password token to not be serialzed") 208 | } 209 | } 210 | 211 | var newMachineTests = []struct { 212 | name string 213 | login string 214 | password string 215 | account string 216 | }{ 217 | {"heroku.com", "dodging-samurai-42@heroku.com", "octocatdodgeballchampions", "2011+2013"}, 218 | {"bgentry.io", "special@test.com", "noacct", ""}, 219 | {"github.io", "2@test.com", "", "acctwithnopass"}, 220 | {"someotherapi.com", "", "passonly", ""}, 221 | } 222 | 223 | func TestNewMachine(t *testing.T) { 224 | n, err := ParseFile("examples/good.netrc") 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | testNewMachine(t, n) 229 | n = &Netrc{} 230 | testNewMachine(t, n) 231 | 232 | // make sure that tokens without a value are not serialized at all 233 | for _, test := range newMachineTests { 234 | n = &Netrc{} 235 | _ = n.NewMachine(test.name, test.login, test.password, test.account) 236 | 237 | bodyb, _ := n.MarshalText() 238 | body := string(bodyb) 239 | 240 | // ensure desired values are present when they should be 241 | if !strings.Contains(body, "machine") { 242 | t.Errorf("NewMachine() %s missing keyword 'machine'", test.name) 243 | } 244 | if !strings.Contains(body, test.name) { 245 | t.Errorf("NewMachine() %s missing value %q", test.name, test.name) 246 | } 247 | if test.login != "" && !strings.Contains(body, "login "+test.login) { 248 | t.Errorf("NewMachine() %s missing value %q", test.name, "login "+test.login) 249 | } 250 | if test.password != "" && !strings.Contains(body, "password "+test.password) { 251 | t.Errorf("NewMachine() %s missing value %q", test.name, "password "+test.password) 252 | } 253 | if test.account != "" && !strings.Contains(body, "account "+test.account) { 254 | t.Errorf("NewMachine() %s missing value %q", test.name, "account "+test.account) 255 | } 256 | 257 | // ensure undesired values are not present when they shouldn't be 258 | if test.login == "" && strings.Contains(body, "login") { 259 | t.Errorf("NewMachine() %s contains unexpected value %q", test.name, "login") 260 | } 261 | if test.password == "" && strings.Contains(body, "password") { 262 | t.Errorf("NewMachine() %s contains unexpected value %q", test.name, "password") 263 | } 264 | if test.account == "" && strings.Contains(body, "account") { 265 | t.Errorf("NewMachine() %s contains unexpected value %q", test.name, "account") 266 | } 267 | } 268 | } 269 | 270 | func testNewMachine(t *testing.T, n *Netrc) { 271 | for _, test := range newMachineTests { 272 | mcount := len(n.machines) 273 | // sanity check 274 | bodyb, _ := n.MarshalText() 275 | body := string(bodyb) 276 | for _, value := range []string{test.name, test.login, test.password, test.account} { 277 | if value != "" && strings.Contains(body, value) { 278 | t.Errorf("MarshalText() before NewMachine() contained unexpected %q", value) 279 | } 280 | } 281 | 282 | // test prefix for machine token 283 | prefix := "\n" 284 | if len(n.tokens) == 0 { 285 | prefix = "" 286 | } 287 | 288 | m := n.NewMachine(test.name, test.login, test.password, test.account) 289 | if m == nil { 290 | t.Fatalf("NewMachine() returned nil") 291 | } 292 | 293 | if len(n.machines) != mcount+1 { 294 | t.Errorf("n.machines count expected %d, got %d", mcount+1, len(n.machines)) 295 | } 296 | // check values 297 | if m.Name != test.name { 298 | t.Errorf("m.Name expected %q, got %q", test.name, m.Name) 299 | } 300 | if m.Login != test.login { 301 | t.Errorf("m.Login expected %q, got %q", test.login, m.Login) 302 | } 303 | if m.Password != test.password { 304 | t.Errorf("m.Password expected %q, got %q", test.password, m.Password) 305 | } 306 | if m.Account != test.account { 307 | t.Errorf("m.Account expected %q, got %q", test.account, m.Account) 308 | } 309 | // check tokens 310 | checkToken(t, "nametoken", m.nametoken, tkMachine, prefix+"machine", test.name) 311 | checkToken(t, "logintoken", m.logintoken, tkLogin, "\n\tlogin", test.login) 312 | checkToken(t, "passtoken", m.passtoken, tkPassword, "\n\tpassword", test.password) 313 | checkToken(t, "accounttoken", m.accounttoken, tkAccount, "\n\taccount", test.account) 314 | // check marshal output 315 | bodyb, _ = n.MarshalText() 316 | body = string(bodyb) 317 | for _, value := range []string{test.name, test.login, test.password, test.account} { 318 | if !strings.Contains(body, value) { 319 | t.Errorf("MarshalText() after NewMachine() did not include %q as expected", value) 320 | } 321 | } 322 | } 323 | } 324 | 325 | func checkToken(t *testing.T, name string, tok *token, kind tkType, rawkind, value string) { 326 | if tok == nil { 327 | t.Errorf("%s not defined", name) 328 | return 329 | } 330 | if tok.kind != kind { 331 | t.Errorf("%s expected kind %d, got %d", name, kind, tok.kind) 332 | } 333 | if string(tok.rawkind) != rawkind { 334 | t.Errorf("%s expected rawkind %q, got %q", name, rawkind, string(tok.rawkind)) 335 | } 336 | if tok.value != value { 337 | t.Errorf("%s expected value %q, got %q", name, value, tok.value) 338 | } 339 | if tok.value != value { 340 | t.Errorf("%s expected value %q, got %q", name, value, tok.value) 341 | } 342 | } 343 | 344 | func TestNewMachineGoesBeforeDefault(t *testing.T) { 345 | n, err := ParseFile("examples/good.netrc") 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | m := n.NewMachine("mymachine", "mylogin", "mypassword", "myaccount") 350 | if m2 := n.machines[len(n.machines)-2]; m2 != m { 351 | t.Errorf("expected machine %v, got %v", m, m2) 352 | } 353 | } 354 | 355 | func TestRemoveMachine(t *testing.T) { 356 | n, err := ParseFile("examples/good.netrc") 357 | if err != nil { 358 | t.Fatal(err) 359 | } 360 | 361 | tests := []string{"mail.google.com", "weirdlogin"} 362 | 363 | for _, name := range tests { 364 | mcount := len(n.machines) 365 | // sanity check 366 | m := n.FindMachine(name) 367 | if m == nil { 368 | t.Fatalf("machine %q not found", name) 369 | } 370 | if m.IsDefault() { 371 | t.Fatalf("expected machine %q, got default instead", name) 372 | } 373 | n.RemoveMachine(name) 374 | 375 | if len(n.machines) != mcount-1 { 376 | t.Errorf("n.machines count expected %d, got %d", mcount-1, len(n.machines)) 377 | } 378 | 379 | // make sure Machine is no longer returned by FindMachine() 380 | if m2 := n.FindMachine(name); m2 != nil && !m2.IsDefault() { 381 | t.Errorf("Machine %q not removed from Machines list", name) 382 | } 383 | 384 | // make sure tokens are not present in tokens list 385 | for _, token := range []*token{m.nametoken, m.logintoken, m.passtoken, m.accounttoken} { 386 | if token != nil { 387 | for _, tok2 := range n.tokens { 388 | if tok2 == token { 389 | t.Errorf("token not removed from tokens list: %v", token) 390 | break 391 | } 392 | } 393 | } 394 | } 395 | 396 | bodyb, _ := n.MarshalText() 397 | body := string(bodyb) 398 | for _, value := range []string{m.Name, m.Login, m.Password, m.Account} { 399 | if value != "" && strings.Contains(body, value) { 400 | t.Errorf("MarshalText() after RemoveMachine() contained unexpected %q", value) 401 | } 402 | } 403 | } 404 | } 405 | 406 | func TestUpdateLogin(t *testing.T) { 407 | n, err := ParseFile("examples/good.netrc") 408 | if err != nil { 409 | t.Fatal(err) 410 | } 411 | 412 | tests := []struct { 413 | exists bool 414 | name string 415 | oldlogin string 416 | newlogin string 417 | }{ 418 | {true, "mail.google.com", "joe@gmail.com", "joe2@gmail.com"}, 419 | {false, "heroku.com", "", "dodging-samurai-42@heroku.com"}, 420 | } 421 | 422 | bodyb, _ := n.MarshalText() 423 | body := string(bodyb) 424 | for _, test := range tests { 425 | if strings.Contains(body, test.newlogin) { 426 | t.Errorf("MarshalText() before UpdateLogin() contained unexpected %q", test.newlogin) 427 | } 428 | } 429 | 430 | for _, test := range tests { 431 | m := n.FindMachine(test.name) 432 | if m.IsDefault() == test.exists { 433 | t.Errorf("expected machine %s to not exist, but it did", test.name) 434 | } else { 435 | if !test.exists { 436 | m = n.NewMachine(test.name, test.newlogin, "", "") 437 | } 438 | if m == nil { 439 | t.Errorf("machine %s was nil", test.name) 440 | continue 441 | } 442 | m.UpdateLogin(test.newlogin) 443 | m := n.FindMachine(test.name) 444 | if m.Login != test.newlogin { 445 | t.Errorf("expected new login %q, got %q", test.newlogin, m.Login) 446 | } 447 | if m.logintoken.value != test.newlogin { 448 | t.Errorf("expected m.logintoken %q, got %q", test.newlogin, m.logintoken.value) 449 | } 450 | } 451 | } 452 | 453 | bodyb, _ = n.MarshalText() 454 | body = string(bodyb) 455 | for _, test := range tests { 456 | if test.exists && strings.Contains(body, test.oldlogin) { 457 | t.Errorf("MarshalText() after UpdateLogin() contained unexpected %q", test.oldlogin) 458 | } 459 | if !strings.Contains(body, test.newlogin) { 460 | t.Errorf("MarshalText after UpdatePassword did not contain %q as expected", test.newlogin) 461 | } 462 | } 463 | } 464 | 465 | func TestUpdatePassword(t *testing.T) { 466 | n, err := ParseFile("examples/good.netrc") 467 | if err != nil { 468 | t.Fatal(err) 469 | } 470 | 471 | tests := []struct { 472 | exists bool 473 | name string 474 | oldpassword string 475 | newpassword string 476 | }{ 477 | {true, "ray", "mypassword", "supernewpass"}, 478 | {false, "heroku.com", "", "octocatdodgeballchampions"}, 479 | } 480 | 481 | bodyb, _ := n.MarshalText() 482 | body := string(bodyb) 483 | for _, test := range tests { 484 | if test.exists && !strings.Contains(body, test.oldpassword) { 485 | t.Errorf("MarshalText() before UpdatePassword() did not include %q as expected", test.oldpassword) 486 | } 487 | if strings.Contains(body, test.newpassword) { 488 | t.Errorf("MarshalText() before UpdatePassword() contained unexpected %q", test.newpassword) 489 | } 490 | } 491 | 492 | for _, test := range tests { 493 | m := n.FindMachine(test.name) 494 | if m.IsDefault() == test.exists { 495 | t.Errorf("expected machine %s to not exist, but it did", test.name) 496 | } else { 497 | if !test.exists { 498 | m = n.NewMachine(test.name, "", test.newpassword, "") 499 | } 500 | if m == nil { 501 | t.Errorf("machine %s was nil", test.name) 502 | continue 503 | } 504 | m.UpdatePassword(test.newpassword) 505 | m = n.FindMachine(test.name) 506 | if m.Password != test.newpassword { 507 | t.Errorf("expected new password %q, got %q", test.newpassword, m.Password) 508 | } 509 | if m.passtoken.value != test.newpassword { 510 | t.Errorf("expected m.passtoken %q, got %q", test.newpassword, m.passtoken.value) 511 | } 512 | } 513 | } 514 | 515 | bodyb, _ = n.MarshalText() 516 | body = string(bodyb) 517 | for _, test := range tests { 518 | if test.exists && strings.Contains(body, test.oldpassword) { 519 | t.Errorf("MarshalText() after UpdatePassword() contained unexpected %q", test.oldpassword) 520 | } 521 | if !strings.Contains(body, test.newpassword) { 522 | t.Errorf("MarshalText() after UpdatePassword() did not contain %q as expected", test.newpassword) 523 | } 524 | } 525 | } 526 | 527 | func TestNewFile(t *testing.T) { 528 | var n Netrc 529 | 530 | result, err := n.MarshalText() 531 | if err != nil { 532 | t.Fatal(err) 533 | } 534 | if string(result) != "" { 535 | t.Errorf("expected empty result=\"\", got %q", string(result)) 536 | } 537 | 538 | n.NewMachine("netrctest.heroku.com", "auser", "apassword", "") 539 | 540 | result, err = n.MarshalText() 541 | if err != nil { 542 | t.Fatal(err) 543 | } 544 | expected := `machine netrctest.heroku.com 545 | login auser 546 | password apassword` 547 | 548 | if string(result) != expected { 549 | t.Errorf("expected result:\n%q\ngot:\n%q", expected, string(result)) 550 | } 551 | } 552 | 553 | func netrcReader(filename string, t *testing.T) io.Reader { 554 | b, err := ioutil.ReadFile(filename) 555 | if err != nil { 556 | t.Fatal(err) 557 | } 558 | return bytes.NewReader(b) 559 | } 560 | --------------------------------------------------------------------------------