Browse Source

Fix time parsing and reduce syscalls

closes #109
Julian Kornberger 8 years ago
parent
commit
352a1d8821
3 changed files with 80 additions and 28 deletions
  1. 5 4
      ftp.go
  2. 38 19
      parse.go
  3. 37 5
      parse_test.go

+ 5 - 4
ftp.go

@@ -350,14 +350,14 @@ func (c *ServerConn) NameList(path string) (entries []string, err error) {
 // List issues a LIST FTP command.
 // List issues a LIST FTP command.
 func (c *ServerConn) List(path string) (entries []*Entry, err error) {
 func (c *ServerConn) List(path string) (entries []*Entry, err error) {
 	var cmd string
 	var cmd string
-	var parseFunc func(string) (*Entry, error)
+	var parser parseFunc
 
 
 	if c.mlstSupported {
 	if c.mlstSupported {
 		cmd = "MLSD"
 		cmd = "MLSD"
-		parseFunc = parseRFC3659ListLine
+		parser = parseRFC3659ListLine
 	} else {
 	} else {
 		cmd = "LIST"
 		cmd = "LIST"
-		parseFunc = parseListLine
+		parser = parseListLine
 	}
 	}
 
 
 	conn, err := c.cmdDataConnFrom(0, "%s %s", cmd, path)
 	conn, err := c.cmdDataConnFrom(0, "%s %s", cmd, path)
@@ -369,8 +369,9 @@ func (c *ServerConn) List(path string) (entries []*Entry, err error) {
 	defer r.Close()
 	defer r.Close()
 
 
 	scanner := bufio.NewScanner(r)
 	scanner := bufio.NewScanner(r)
+	now := time.Now()
 	for scanner.Scan() {
 	for scanner.Scan() {
-		entry, err := parseFunc(scanner.Text())
+		entry, err := parser(scanner.Text(), now)
 		if err == nil {
 		if err == nil {
 			entries = append(entries, entry)
 			entries = append(entries, entry)
 		}
 		}

+ 38 - 19
parse.go

@@ -9,7 +9,9 @@ import (
 
 
 var errUnsupportedListLine = errors.New("Unsupported LIST line")
 var errUnsupportedListLine = errors.New("Unsupported LIST line")
 
 
-var listLineParsers = []func(line string) (*Entry, error){
+type parseFunc func(string, time.Time) (*Entry, error)
+
+var listLineParsers = []parseFunc{
 	parseRFC3659ListLine,
 	parseRFC3659ListLine,
 	parseLsListLine,
 	parseLsListLine,
 	parseDirListLine,
 	parseDirListLine,
@@ -22,7 +24,7 @@ var dirTimeFormats = []string{
 }
 }
 
 
 // parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
 // parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
-func parseRFC3659ListLine(line string) (*Entry, error) {
+func parseRFC3659ListLine(line string, now time.Time) (*Entry, error) {
 	iSemicolon := strings.Index(line, ";")
 	iSemicolon := strings.Index(line, ";")
 	iWhitespace := strings.Index(line, " ")
 	iWhitespace := strings.Index(line, " ")
 
 
@@ -66,7 +68,7 @@ func parseRFC3659ListLine(line string) (*Entry, error) {
 
 
 // parseLsListLine parses a directory line in a format based on the output of
 // parseLsListLine parses a directory line in a format based on the output of
 // the UNIX ls command.
 // the UNIX ls command.
-func parseLsListLine(line string) (*Entry, error) {
+func parseLsListLine(line string, now time.Time) (*Entry, error) {
 
 
 	// Has the first field a length of 10 bytes?
 	// Has the first field a length of 10 bytes?
 	if strings.IndexByte(line, ' ') != 10 {
 	if strings.IndexByte(line, ' ') != 10 {
@@ -85,7 +87,7 @@ func parseLsListLine(line string) (*Entry, error) {
 			Type: EntryTypeFolder,
 			Type: EntryTypeFolder,
 			Name: scanner.Remaining(),
 			Name: scanner.Remaining(),
 		}
 		}
-		if err := e.setTime(fields[3:6]); err != nil {
+		if err := e.setTime(fields[3:6], now); err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
@@ -102,7 +104,7 @@ func parseLsListLine(line string) (*Entry, error) {
 		if err := e.setSize(fields[2]); err != nil {
 		if err := e.setSize(fields[2]); err != nil {
 			return nil, errUnsupportedListLine
 			return nil, errUnsupportedListLine
 		}
 		}
-		if err := e.setTime(fields[4:7]); err != nil {
+		if err := e.setTime(fields[4:7], now); err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
@@ -132,7 +134,7 @@ func parseLsListLine(line string) (*Entry, error) {
 		return nil, errors.New("Unknown entry type")
 		return nil, errors.New("Unknown entry type")
 	}
 	}
 
 
-	if err := e.setTime(fields[5:8]); err != nil {
+	if err := e.setTime(fields[5:8], now); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -141,7 +143,7 @@ func parseLsListLine(line string) (*Entry, error) {
 
 
 // parseDirListLine parses a directory line in a format based on the output of
 // parseDirListLine parses a directory line in a format based on the output of
 // the MS-DOS DIR command.
 // the MS-DOS DIR command.
-func parseDirListLine(line string) (*Entry, error) {
+func parseDirListLine(line string, now time.Time) (*Entry, error) {
 	e := &Entry{}
 	e := &Entry{}
 	var err error
 	var err error
 
 
@@ -185,7 +187,7 @@ func parseDirListLine(line string) (*Entry, error) {
 // by hostedftp.com
 // by hostedftp.com
 // -r--------   0 user group     65222236 Feb 24 00:39 UABlacklistingWeek8.csv
 // -r--------   0 user group     65222236 Feb 24 00:39 UABlacklistingWeek8.csv
 // (The link count is inexplicably 0)
 // (The link count is inexplicably 0)
-func parseHostedFTPLine(line string) (*Entry, error) {
+func parseHostedFTPLine(line string, now time.Time) (*Entry, error) {
 	// Has the first field a length of 10 bytes?
 	// Has the first field a length of 10 bytes?
 	if strings.IndexByte(line, ' ') != 10 {
 	if strings.IndexByte(line, ' ') != 10 {
 		return nil, errUnsupportedListLine
 		return nil, errUnsupportedListLine
@@ -199,14 +201,14 @@ func parseHostedFTPLine(line string) (*Entry, error) {
 	}
 	}
 
 
 	// Set link count to 1 and attempt to parse as Unix.
 	// Set link count to 1 and attempt to parse as Unix.
-	return parseLsListLine(fields[0] + " 1 " + scanner.Remaining())
+	return parseLsListLine(fields[0]+" 1 "+scanner.Remaining(), now)
 }
 }
 
 
 // parseListLine parses the various non-standard format returned by the LIST
 // parseListLine parses the various non-standard format returned by the LIST
 // FTP command.
 // FTP command.
-func parseListLine(line string) (*Entry, error) {
+func parseListLine(line string, now time.Time) (*Entry, error) {
 	for _, f := range listLineParsers {
 	for _, f := range listLineParsers {
-		e, err := f(line)
+		e, err := f(line, now)
 		if err != errUnsupportedListLine {
 		if err != errUnsupportedListLine {
 			return e, err
 			return e, err
 		}
 		}
@@ -219,17 +221,34 @@ func (e *Entry) setSize(str string) (err error) {
 	return
 	return
 }
 }
 
 
-func (e *Entry) setTime(fields []string) (err error) {
-	var timeStr string
-	if strings.Contains(fields[2], ":") { // this year
-		thisYear, _, _ := time.Now().Date()
-		timeStr = fields[1] + " " + fields[0] + " " + strconv.Itoa(thisYear)[2:4] + " " + fields[2] + " GMT"
-	} else { // not this year
+func (e *Entry) setTime(fields []string, now time.Time) (err error) {
+	if strings.Contains(fields[2], ":") { // contains time
+		thisYear, _, _ := now.Date()
+		timeStr := fields[1] + " " + fields[0] + " " + strconv.Itoa(thisYear)[2:4] + " " + fields[2] + " GMT"
+		e.Time, err = time.Parse("_2 Jan 06 15:04 MST", timeStr)
+
+		/*
+			On unix, `info ls` shows:
+
+			10.1.6 Formatting file timestamps
+			---------------------------------
+
+			A timestamp is considered to be “recent” if it is less than six
+			months old, and is not dated in the future.  If a timestamp dated today
+			is not listed in recent form, the timestamp is in the future, which
+			means you probably have clock skew problems which may break programs
+			like ‘make’ that rely on file timestamps.
+		*/
+		if !e.Time.Before(now.AddDate(0, 6, 0)) {
+			e.Time = e.Time.AddDate(-1, 0, 0)
+		}
+
+	} else { // only the date
 		if len(fields[2]) != 4 {
 		if len(fields[2]) != 4 {
 			return errors.New("Invalid year format in time string")
 			return errors.New("Invalid year format in time string")
 		}
 		}
-		timeStr = fields[1] + " " + fields[0] + " " + fields[2][2:4] + " 00:00 GMT"
+		timeStr := fields[1] + " " + fields[0] + " " + fields[2][2:4] + " 00:00 GMT"
+		e.Time, err = time.Parse("_2 Jan 06 15:04 MST", timeStr)
 	}
 	}
-	e.Time, err = time.Parse("_2 Jan 06 15:04 MST", timeStr)
 	return
 	return
 }
 }

+ 37 - 5
parse_test.go

@@ -1,11 +1,18 @@
 package ftp
 package ftp
 
 
 import (
 import (
+	"strings"
 	"testing"
 	"testing"
 	"time"
 	"time"
 )
 )
 
 
-var thisYear, _, _ = time.Now().Date()
+var (
+	// now is the current time for all tests
+	now = time.Date(2017, time.March, 10, 23, 0, 0, 0, time.UTC)
+
+	thisYear, _, _ = now.Date()
+	previousYear   = thisYear - 1
+)
 
 
 type line struct {
 type line struct {
 	line      string
 	line      string
@@ -36,7 +43,7 @@ var listTests = []line{
 
 
 	// Microsoft's FTP servers for Windows
 	// Microsoft's FTP servers for Windows
 	{"----------   1 owner    group         1803128 Jul 10 10:18 ls-lR.Z", "ls-lR.Z", 1803128, EntryTypeFile, time.Date(thisYear, time.July, 10, 10, 18, 0, 0, time.UTC)},
 	{"----------   1 owner    group         1803128 Jul 10 10:18 ls-lR.Z", "ls-lR.Z", 1803128, EntryTypeFile, time.Date(thisYear, time.July, 10, 10, 18, 0, 0, time.UTC)},
-	{"d---------   1 owner    group               0 May  9 19:45 Softlib", "Softlib", 0, EntryTypeFolder, time.Date(thisYear, time.May, 9, 19, 45, 0, 0, time.UTC)},
+	{"d---------   1 owner    group               0 Nov  9 19:45 Softlib", "Softlib", 0, EntryTypeFolder, time.Date(previousYear, time.November, 9, 19, 45, 0, 0, time.UTC)},
 
 
 	// WFTPD for MSDOS
 	// WFTPD for MSDOS
 	{"-rwxrwxrwx   1 noone    nogroup      322 Aug 19  1996 message.ftp", "message.ftp", 322, EntryTypeFile, time.Date(1996, time.August, 19, 0, 0, 0, 0, time.UTC)},
 	{"-rwxrwxrwx   1 noone    nogroup      322 Aug 19  1996 message.ftp", "message.ftp", 322, EntryTypeFile, time.Date(1996, time.August, 19, 0, 0, 0, 0, time.UTC)},
@@ -77,7 +84,7 @@ var listTestsFail = []unsupportedLine{
 
 
 func TestParseValidListLine(t *testing.T) {
 func TestParseValidListLine(t *testing.T) {
 	for _, lt := range listTests {
 	for _, lt := range listTests {
-		entry, err := parseListLine(lt.line)
+		entry, err := parseListLine(lt.line, now)
 		if err != nil {
 		if err != nil {
 			t.Errorf("parseListLine(%v) returned err = %v", lt.line, err)
 			t.Errorf("parseListLine(%v) returned err = %v", lt.line, err)
 			continue
 			continue
@@ -91,7 +98,7 @@ func TestParseValidListLine(t *testing.T) {
 		if entry.Size != lt.size {
 		if entry.Size != lt.size {
 			t.Errorf("parseListLine(%v).Size = %v, want %v", lt.line, entry.Size, lt.size)
 			t.Errorf("parseListLine(%v).Size = %v, want %v", lt.line, entry.Size, lt.size)
 		}
 		}
-		if entry.Time.Unix() != lt.time.Unix() {
+		if !entry.Time.Equal(lt.time) {
 			t.Errorf("parseListLine(%v).Time = %v, want %v", lt.line, entry.Time, lt.time)
 			t.Errorf("parseListLine(%v).Time = %v, want %v", lt.line, entry.Time, lt.time)
 		}
 		}
 	}
 	}
@@ -99,7 +106,7 @@ func TestParseValidListLine(t *testing.T) {
 
 
 func TestParseUnsupportedListLine(t *testing.T) {
 func TestParseUnsupportedListLine(t *testing.T) {
 	for _, lt := range listTestsFail {
 	for _, lt := range listTestsFail {
-		_, err := parseListLine(lt.line)
+		_, err := parseListLine(lt.line, now)
 		if err == nil {
 		if err == nil {
 			t.Errorf("parseListLine(%v) expected to fail", lt.line)
 			t.Errorf("parseListLine(%v) expected to fail", lt.line)
 		}
 		}
@@ -108,3 +115,28 @@ func TestParseUnsupportedListLine(t *testing.T) {
 		}
 		}
 	}
 	}
 }
 }
+
+func TestSettime(t *testing.T) {
+	tests := []struct {
+		line     string
+		expected time.Time
+	}{
+		// this year, in the past
+		{"Feb 10 23:00", time.Date(thisYear, time.February, 10, 23, 0, 0, 0, time.UTC)},
+
+		// this year, less than six months in the future
+		{"Sep 10 22:59", time.Date(thisYear, time.September, 10, 22, 59, 0, 0, time.UTC)},
+
+		// previous year, otherwise it would be more than 6 months in the future
+		{"Sep 10 23:00", time.Date(previousYear, time.September, 10, 23, 0, 0, 0, time.UTC)},
+	}
+
+	for _, test := range tests {
+		entry := &Entry{}
+		entry.setTime(strings.Fields(test.line), now)
+
+		if !entry.Time.Equal(test.expected) {
+			t.Errorf("setTime(%v).Time = %v, want %v", test.line, entry.Time, test.expected)
+		}
+	}
+}