Skip to content

Commit 2c3ef90

Browse files
committed
new parser and new internal format to preserves whitespace
1 parent 202f370 commit 2c3ef90

File tree

5 files changed

+238
-120
lines changed

5 files changed

+238
-120
lines changed

LICENSE

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Copyright © 2010 Fazlul Shahriar <[email protected]>.
1+
Original version Copyright © 2010 Fazlul Shahriar <[email protected]>. Newer
2+
portions Copyright © 2014 Blake Gentry <[email protected]>.
23

34
Permission is hereby granted, free of charge, to any person obtaining a copy
45
of this software and associated documentation files (the "Software"), to deal

netrc/examples/bad_default_order.netrc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ machine mail.google.com
55
password somethingSecret
66
# I am another comment
77

8-
macdef allput
9-
put src/*
10-
118
default
129
login anonymous
1310

netrc/examples/good.netrc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
# I am a comment
22
machine mail.google.com
33
4-
account gmail
4+
account gmail #end of line comment with trailing space
55
password somethingSecret
6-
# I am another comment
6+
# I am another comment
77

88
macdef allput
99
put src/*
1010

11+
macdef allput2
12+
put src/*
13+
put src2/*
14+
1115
machine ray login demo password mypassword
1216

17+
machine weirdlogin login uname password pass#pass
18+
1319
default
1420
login anonymous
1521

netrc/netrc.go

Lines changed: 157 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
// Copyright © 2010 Fazlul Shahriar <[email protected]>.
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 .
101
package netrc
112

123
import (
@@ -17,21 +8,25 @@ import (
178
"io"
189
"io/ioutil"
1910
"os"
11+
"strings"
2012
"unicode"
2113
"unicode/utf8"
2214
)
2315

16+
type tkType int
17+
2418
const (
25-
tkMachine = iota
19+
tkMachine tkType = iota
2620
tkDefault
2721
tkLogin
2822
tkPassword
2923
tkAccount
3024
tkMacdef
3125
tkComment
26+
tkWhitespace
3227
)
3328

34-
var keywords = map[string]int{
29+
var keywords = map[string]tkType{
3530
"machine": tkMachine,
3631
"default": tkDefault,
3732
"login": tkLogin,
@@ -76,9 +71,11 @@ type Machine struct {
7671
type Macros map[string]string
7772

7873
type token struct {
79-
kind int
74+
kind tkType
8075
macroName string
8176
value string
77+
rawkind []byte
78+
rawvalue []byte
8279
}
8380

8481
// Error represents a netrc file parse error.
@@ -98,118 +95,159 @@ func (e *Error) BadDefaultOrder() bool {
9895

9996
const errBadDefaultOrder = "default token must appear after all machine tokens"
10097

101-
func getToken(b []byte, pos int) ([]byte, *token, error) {
102-
adv, wordb, err := bufio.ScanWords(b, true)
103-
if err != nil {
104-
return b, nil, err // should never happen
98+
// scanLinesKeepPrefix is a split function for a Scanner that returns each line
99+
// of text. The returned token may include newlines if they are before the
100+
// first non-space character. The returned line may be empty. The end-of-line
101+
// marker is one optional carriage return followed by one mandatory newline. In
102+
// regular expression notation, it is `\r?\n`. The last non-empty line of
103+
// input will be returned even if it has no newline.
104+
func scanLinesKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) {
105+
if atEOF && len(data) == 0 {
106+
return 0, nil, nil
105107
}
106-
b = b[adv:]
107-
word := string(wordb)
108-
if word == "" {
109-
return b, nil, nil // EOF reached
108+
// Skip leading spaces.
109+
start := 0
110+
for width := 0; start < len(data); start += width {
111+
var r rune
112+
r, width = utf8.DecodeRune(data[start:])
113+
if !unicode.IsSpace(r) {
114+
break
115+
}
110116
}
111-
112-
t := new(token)
113-
var ok bool
114-
t.kind, ok = keywords[word]
115-
if !ok {
116-
return b, nil, &Error{pos, "keyword expected; got " + word}
117+
if i := bytes.IndexByte(data[start:], '\n'); i >= 0 {
118+
// We have a full newline-terminated line.
119+
return start + i, data[0 : start+i], nil
117120
}
118-
if t.kind == tkDefault {
119-
return b, t, nil
121+
// If we're at EOF, we have a final, non-terminated line. Return it.
122+
if atEOF {
123+
return len(data), data, nil
120124
}
121-
if t.kind == tkComment {
122-
t.value = word + " "
123-
adv, wordb, err = bufio.ScanLines(b, true)
124-
if err != nil {
125-
return b, nil, err // should never happen
125+
// Request more data.
126+
return 0, nil, nil
127+
}
128+
129+
// scanWordsKeepPrefix is a split function for a Scanner that returns each
130+
// space-separated word of text, with prefixing spaces included. It will never
131+
// return an empty string. The definition of space is set by unicode.IsSpace.
132+
//
133+
// Adapted from bufio.ScanWords().
134+
func scanTokensKeepPrefix(data []byte, atEOF bool) (advance int, token []byte, err error) {
135+
// Skip leading spaces.
136+
start := 0
137+
for width := 0; start < len(data); start += width {
138+
var r rune
139+
r, width = utf8.DecodeRune(data[start:])
140+
if !unicode.IsSpace(r) {
141+
break
126142
}
127-
t.value = t.value + string(wordb)
128-
return b[adv:], t, nil
129143
}
130-
131-
if word == "" {
132-
return b, nil, &Error{pos, "word expected"}
144+
if atEOF && len(data) == 0 {
145+
return 0, nil, nil
133146
}
134-
if t.kind == tkMacdef {
135-
adv, lineb, err := bufio.ScanLines(b, true)
136-
if err != nil {
137-
return b, nil, err // should never happen
138-
}
139-
b = b[adv:]
140-
adv, wordb, err = bufio.ScanWords(lineb, true)
141-
if err != nil {
142-
return b, nil, err // should never happen
147+
if len(data) > start && data[start] == '#' {
148+
return scanLinesKeepPrefix(data, atEOF)
149+
}
150+
// Scan until space, marking end of word.
151+
for width, i := 0, start; i < len(data); i += width {
152+
var r rune
153+
r, width = utf8.DecodeRune(data[i:])
154+
if unicode.IsSpace(r) {
155+
return i, data[:i], nil
143156
}
144-
word = string(wordb)
145-
t.macroName = word
146-
lineb = lineb[adv:]
157+
}
158+
// If we're at EOF, we have a final, non-empty, non-terminated word. Return it.
159+
if atEOF && len(data) > start {
160+
return len(data), data, nil
161+
}
162+
// Request more data.
163+
return 0, nil, nil
164+
}
147165

148-
// Macro value starts on next line. The rest of current line
149-
// should contain nothing but whitespace
150-
i := 0
151-
for i < len(lineb) {
152-
r, size := utf8.DecodeRune(lineb[i:])
153-
if r == '\n' {
154-
i += size
155-
pos++
156-
break
157-
}
158-
if !unicode.IsSpace(r) {
159-
return b, nil, &Error{pos, "unexpected word"}
160-
}
161-
i += size
166+
func newToken(rawb []byte) (*token, error) {
167+
_, tkind, err := bufio.ScanWords(rawb, true)
168+
if err != nil {
169+
return nil, err
170+
}
171+
var ok bool
172+
t := token{rawkind: rawb}
173+
t.kind, ok = keywords[string(tkind)]
174+
if !ok {
175+
trimmed := strings.TrimSpace(string(tkind))
176+
if trimmed == "" {
177+
t.kind = tkWhitespace // whitespace-only, should happen only at EOF
178+
return &t, nil
162179
}
163-
164-
// Find end of macro value
165-
i = bytes.Index(b, []byte("\n\n"))
166-
if i < 0 { // EOF reached
167-
i = len(b)
180+
if strings.HasPrefix(trimmed, "#") {
181+
t.kind = tkComment // this is a comment
182+
return &t, nil
168183
}
169-
t.value = string(b[0:i])
184+
return &t, fmt.Errorf("keyword expected; got " + string(tkind))
185+
}
186+
return &t, nil
187+
}
170188

171-
return b[i:], t, nil
172-
} else {
173-
adv, wordb, err = bufio.ScanWords(b, true)
174-
if err != nil {
175-
return b, nil, err // should never happen
176-
}
177-
word = string(wordb)
178-
b = b[adv:]
189+
func scanValue(scanner *bufio.Scanner, pos int) ([]byte, string, int, error) {
190+
if scanner.Scan() {
191+
raw := scanner.Bytes()
192+
pos += bytes.Count(raw, []byte{'\n'})
193+
return raw, strings.TrimSpace(string(raw)), pos, nil
179194
}
180-
t.value = word
181-
return b, t, nil
195+
if err := scanner.Err(); err != nil {
196+
return nil, "", pos, &Error{pos, err.Error()}
197+
}
198+
return nil, "", pos, nil
182199
}
183200

184201
func parse(r io.Reader, pos int) (*Netrc, error) {
185-
// TODO(fhs): Clear memory containing password.
186202
b, err := ioutil.ReadAll(r)
187203
if err != nil {
188204
return nil, err
189205
}
190206

191-
mach := make([]*Machine, 0, 20)
192-
mac := make(Macros, 10)
193-
var defaultSeen bool
207+
nrc := Netrc{machines: make([]*Machine, 0, 20), macros: make(Macros, 10)}
208+
209+
defaultSeen := false
210+
var currentMacro *token
194211
var m *Machine
195212
var t *token
196-
for {
197-
b, t, err = getToken(b, pos)
213+
scanner := bufio.NewScanner(bytes.NewReader(b))
214+
scanner.Split(scanTokensKeepPrefix)
215+
216+
for scanner.Scan() {
217+
rawb := scanner.Bytes()
218+
if len(rawb) == 0 {
219+
break
220+
}
221+
pos += bytes.Count(rawb, []byte{'\n'})
222+
t, err = newToken(rawb)
198223
if err != nil {
199-
return nil, err
224+
if currentMacro == nil {
225+
return nil, &Error{pos, err.Error()}
226+
}
227+
currentMacro.rawvalue = append(currentMacro.rawvalue, rawb...)
228+
continue
200229
}
201-
if t == nil {
202-
break
230+
231+
if currentMacro != nil && bytes.Contains(rawb, []byte{'\n', '\n'}) {
232+
// if macro rawvalue + rawb would contain \n\n, then macro def is over
233+
currentMacro.value = strings.TrimSpace(string(currentMacro.rawvalue))
234+
nrc.macros[currentMacro.macroName] = currentMacro.value
235+
nrc.tokens = append(nrc.tokens, currentMacro)
236+
currentMacro = nil
203237
}
238+
204239
switch t.kind {
205240
case tkMacdef:
206-
mac[t.macroName] = t.value
241+
if _, t.macroName, pos, err = scanValue(scanner, pos); err != nil {
242+
return nil, &Error{pos, err.Error()}
243+
}
244+
currentMacro = t
207245
case tkDefault:
208246
if defaultSeen {
209247
return nil, &Error{pos, "multiple default token"}
210248
}
211249
if m != nil {
212-
mach, m = append(mach, m), nil
250+
nrc.machines, m = append(nrc.machines, m), nil
213251
}
214252
m = new(Machine)
215253
m.Name = ""
@@ -219,37 +257,57 @@ func parse(r io.Reader, pos int) (*Netrc, error) {
219257
return nil, &Error{pos, errBadDefaultOrder}
220258
}
221259
if m != nil {
222-
mach, m = append(mach, m), nil
260+
nrc.machines, m = append(nrc.machines, m), nil
223261
}
224262
m = new(Machine)
225-
m.Name = t.value
263+
if t.rawvalue, m.Name, pos, err = scanValue(scanner, pos); err != nil {
264+
return nil, &Error{pos, err.Error()}
265+
}
266+
t.value = m.Name
226267
case tkLogin:
227268
if m == nil || m.Login != "" {
228269
return nil, &Error{pos, "unexpected token login "}
229270
}
230-
m.Login = t.value
271+
if t.rawvalue, m.Login, pos, err = scanValue(scanner, pos); err != nil {
272+
return nil, &Error{pos, err.Error()}
273+
}
274+
t.value = m.Login
231275
case tkPassword:
232276
if m == nil || m.Password != "" {
233277
return nil, &Error{pos, "unexpected token password"}
234278
}
235-
m.Password = t.value
279+
if t.rawvalue, m.Password, pos, err = scanValue(scanner, pos); err != nil {
280+
return nil, &Error{pos, err.Error()}
281+
}
282+
t.value = m.Password
236283
case tkAccount:
237284
if m == nil || m.Account != "" {
238285
return nil, &Error{pos, "unexpected token account"}
239286
}
240-
m.Account = t.value
287+
if t.rawvalue, m.Account, pos, err = scanValue(scanner, pos); err != nil {
288+
return nil, &Error{pos, err.Error()}
289+
}
290+
t.value = m.Account
291+
case tkComment:
292+
// read whole line
241293
}
294+
295+
nrc.tokens = append(nrc.tokens, t)
296+
}
297+
298+
if err := scanner.Err(); err != nil {
299+
return nil, err
242300
}
301+
243302
if m != nil {
244-
mach, m = append(mach, m), nil
303+
nrc.machines, m = append(nrc.machines, m), nil
245304
}
246-
return &Netrc{machines: mach, macros: mac}, nil
305+
return &nrc, nil
247306
}
248307

249308
// ParseFile opens the file at filename and then passes its io.Reader to
250309
// Parse().
251310
func ParseFile(filename string) (*Netrc, error) {
252-
// TODO(fhs): Check if file is readable by anyone besides the user if there is password in it.
253311
fd, err := os.Open(filename)
254312
if err != nil {
255313
return nil, err

0 commit comments

Comments
 (0)