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 .
10
1
package netrc
11
2
12
3
import (
@@ -17,21 +8,25 @@ import (
17
8
"io"
18
9
"io/ioutil"
19
10
"os"
11
+ "strings"
20
12
"unicode"
21
13
"unicode/utf8"
22
14
)
23
15
16
+ type tkType int
17
+
24
18
const (
25
- tkMachine = iota
19
+ tkMachine tkType = iota
26
20
tkDefault
27
21
tkLogin
28
22
tkPassword
29
23
tkAccount
30
24
tkMacdef
31
25
tkComment
26
+ tkWhitespace
32
27
)
33
28
34
- var keywords = map [string ]int {
29
+ var keywords = map [string ]tkType {
35
30
"machine" : tkMachine ,
36
31
"default" : tkDefault ,
37
32
"login" : tkLogin ,
@@ -76,9 +71,11 @@ type Machine struct {
76
71
type Macros map [string ]string
77
72
78
73
type token struct {
79
- kind int
74
+ kind tkType
80
75
macroName string
81
76
value string
77
+ rawkind []byte
78
+ rawvalue []byte
82
79
}
83
80
84
81
// Error represents a netrc file parse error.
@@ -98,118 +95,159 @@ func (e *Error) BadDefaultOrder() bool {
98
95
99
96
const errBadDefaultOrder = "default token must appear after all machine tokens"
100
97
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
105
107
}
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
+ }
110
116
}
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
117
120
}
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
120
124
}
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
126
142
}
127
- t .value = t .value + string (wordb )
128
- return b [adv :], t , nil
129
143
}
130
-
131
- if word == "" {
132
- return b , nil , & Error {pos , "word expected" }
144
+ if atEOF && len (data ) == 0 {
145
+ return 0 , nil , nil
133
146
}
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
143
156
}
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
+ }
147
165
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
162
179
}
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
168
183
}
169
- t .value = string (b [0 :i ])
184
+ return & t , fmt .Errorf ("keyword expected; got " + string (tkind ))
185
+ }
186
+ return & t , nil
187
+ }
170
188
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
179
194
}
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
182
199
}
183
200
184
201
func parse (r io.Reader , pos int ) (* Netrc , error ) {
185
- // TODO(fhs): Clear memory containing password.
186
202
b , err := ioutil .ReadAll (r )
187
203
if err != nil {
188
204
return nil , err
189
205
}
190
206
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
194
211
var m * Machine
195
212
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 )
198
223
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
200
229
}
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
203
237
}
238
+
204
239
switch t .kind {
205
240
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
207
245
case tkDefault :
208
246
if defaultSeen {
209
247
return nil , & Error {pos , "multiple default token" }
210
248
}
211
249
if m != nil {
212
- mach , m = append (mach , m ), nil
250
+ nrc . machines , m = append (nrc . machines , m ), nil
213
251
}
214
252
m = new (Machine )
215
253
m .Name = ""
@@ -219,37 +257,57 @@ func parse(r io.Reader, pos int) (*Netrc, error) {
219
257
return nil , & Error {pos , errBadDefaultOrder }
220
258
}
221
259
if m != nil {
222
- mach , m = append (mach , m ), nil
260
+ nrc . machines , m = append (nrc . machines , m ), nil
223
261
}
224
262
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
226
267
case tkLogin :
227
268
if m == nil || m .Login != "" {
228
269
return nil , & Error {pos , "unexpected token login " }
229
270
}
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
231
275
case tkPassword :
232
276
if m == nil || m .Password != "" {
233
277
return nil , & Error {pos , "unexpected token password" }
234
278
}
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
236
283
case tkAccount :
237
284
if m == nil || m .Account != "" {
238
285
return nil , & Error {pos , "unexpected token account" }
239
286
}
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
241
293
}
294
+
295
+ nrc .tokens = append (nrc .tokens , t )
296
+ }
297
+
298
+ if err := scanner .Err (); err != nil {
299
+ return nil , err
242
300
}
301
+
243
302
if m != nil {
244
- mach , m = append (mach , m ), nil
303
+ nrc . machines , m = append (nrc . machines , m ), nil
245
304
}
246
- return & Netrc { machines : mach , macros : mac } , nil
305
+ return & nrc , nil
247
306
}
248
307
249
308
// ParseFile opens the file at filename and then passes its io.Reader to
250
309
// Parse().
251
310
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.
253
311
fd , err := os .Open (filename )
254
312
if err != nil {
255
313
return nil , err
0 commit comments