diff --git a/parse.go b/parse.go index 2decf1e..438bea3 100644 --- a/parse.go +++ b/parse.go @@ -19,6 +19,7 @@ var listLineParsers = []parseFunc{ parseLsListLine, parseDirListLine, parseHostedFTPLine, + parseVMSFTPLine, } var dirTimeFormats = []string{ @@ -26,6 +27,73 @@ var dirTimeFormats = []string{ "2006-01-02 15:04", } +// Empty string that saves the last string for VMS +var previousString = "" + +func parseVMSFTPLine(s string, _ time.Time, location *time.Location) (*Entry, error) { + //If the string is empty, there are continuations on the next line(s) + if s == "" { + return nil, errUnsupportedListLine + } + + scanner := newScanner(s) + filename := scanner.NextFields(1)[0] + + // If the line does not contain a semicolon and there is nothing in previousString it is not a VMS FTP filename line + if !strings.Contains(filename, ";") && previousString == "" { + return nil, errUnsupportedListLine + } + + remainingFields := scanner.NextFields(5) + + // If there are no more fields then the current line has a continuation on the next line + if len(remainingFields) == 0 { + previousString = filename + return nil, errUnsupportedListLine + } + + // If there is a previousString, then the current line is a continuation of the previous line + // Insert the current filename in remainingFields and set filename to previousString + if previousString != "" { + remainingFields = append([]string{filename}, remainingFields...) + filename = previousString + // Reset previousString + previousString = "" + } + + if len(remainingFields) < 5 { + return nil, errUnsupportedListLine + } + + entry := &Entry{} + // Files are formatted like this: + // FILENAME.EXT;1 123/125 12-DEC-2017 14:10:37 [GROUP,OWNER] (RWED,RWED,RE,) + // Directories are formatted like this: + // DIRECTORY.DIR;1 123/125 12-DEC-2017 14:10:37 [GROUP,OWNER] (RWED,RWED,RE,) + + // Remove the version + parsedNameUnix := strings.Split(filename, ";")[0] + + if strings.Contains(filename, ".DIR;") { + // Strip .DIR from parsedNameUnix + entry.Name = strings.Replace(parsedNameUnix, ".DIR", "", 1) + entry.Type = EntryTypeFolder + entry.Size = 0 + } else { + entry.Name = parsedNameUnix + entry.Type = EntryTypeFile + // First number is the blocks used + parsedSize := strings.Split(remainingFields[0], "/")[0] + + _ = entry.setSize(parsedSize) + } + + // Parse the date + entry.Time, _ = time.ParseInLocation("_2-Jan-2006 15:04:05", remainingFields[1]+" "+remainingFields[2], location) + + return entry, nil +} + // parseRFC3659ListLine parses the style of directory line defined in RFC 3659. func parseRFC3659ListLine(line string, _ time.Time, loc *time.Location) (*Entry, error) { return parseNextRFC3659ListLine(line, loc, &Entry{}) @@ -164,7 +232,7 @@ func parseLsListLine(line string, now time.Time, loc *time.Location) (*Entry, er // parseDirListLine parses a directory line in a format based on the output of // the MS-DOS DIR command. -func parseDirListLine(line string, now time.Time, loc *time.Location) (*Entry, error) { +func parseDirListLine(line string, _ time.Time, loc *time.Location) (*Entry, error) { e := &Entry{} var err error diff --git a/parse_test.go b/parse_test.go index 8bcd45a..31f9b75 100644 --- a/parse_test.go +++ b/parse_test.go @@ -79,6 +79,10 @@ var listTests = []line{ // Line with ACL persmissions {"-rwxrw-r--+ 1 521 101 2080 May 21 10:53 data.csv", "data.csv", 2080, EntryTypeFile, newTime(thisYear, time.May, 21, 10, 53)}, + + // OPENVMS style + {"DATA.DIR;1 1/576 4-JAN-2022 14:14:36 [SCANCO,MICROCT] (RWE,RWE,RE,)", "DATA", 0, EntryTypeFolder, newTime(2022, time.January, 4, 14, 14, 36)}, + {"DECW$SM.LOG;247 0/576 17-MAY-2023 15:20:28 [SCANCO,MICROCT] (RWED,RWED,RE,)", "DECW$SM.LOG", 0, EntryTypeFile, newTime(2023, time.May, 17, 15, 20, 28)}, } var listTestsSymlink = []symlinkLine{ @@ -96,19 +100,20 @@ var listTestsFail = []unsupportedLine{ {"total 1", errUnsupportedListLine}, {"000000000x ", errUnsupportedListLine}, // see https://github.com/jlaffaye/ftp/issues/97 {"", errUnsupportedListLine}, + {"DECW$SM.LOG;247", errUnsupportedListLine}, //This is the case where VMS has a continuation on the next line } func TestParseValidListLine(t *testing.T) { for _, lt := range listTests { t.Run(lt.line, func(t *testing.T) { - assert := assert.New(t) + assertions := assert.New(t) entry, err := parseListLine(lt.line, now, time.UTC) - if assert.NoError(err) { - assert.Equal(lt.name, entry.Name) - assert.Equal(lt.entryType, entry.Type) - assert.Equal(lt.size, entry.Size) - assert.Equal(lt.time, entry.Time) + if assertions.NoError(err) { + assertions.Equal(lt.name, entry.Name) + assertions.Equal(lt.entryType, entry.Type) + assertions.Equal(lt.size, entry.Size) + assertions.Equal(lt.time, entry.Time) } }) } @@ -117,13 +122,13 @@ func TestParseValidListLine(t *testing.T) { func TestParseSymlinks(t *testing.T) { for _, lt := range listTestsSymlink { t.Run(lt.line, func(t *testing.T) { - assert := assert.New(t) + assertions := assert.New(t) entry, err := parseListLine(lt.line, now, time.UTC) - if assert.NoError(err) { - assert.Equal(lt.name, entry.Name) - assert.Equal(lt.target, entry.Target) - assert.Equal(EntryTypeLink, entry.Type) + if assertions.NoError(err) { + assertions.Equal(lt.name, entry.Name) + assertions.Equal(lt.target, entry.Target) + assertions.Equal(EntryTypeLink, entry.Type) } }) } @@ -171,7 +176,7 @@ func TestSettime(t *testing.T) { // newTime builds a UTC time from the given year, month, day, hour and minute func newTime(year int, month time.Month, day int, hourMinSec ...int) time.Time { - var hour, min, sec int + var hour, minute, sec int switch len(hourMinSec) { case 0: @@ -180,7 +185,7 @@ func newTime(year int, month time.Month, day int, hourMinSec ...int) time.Time { sec = hourMinSec[2] fallthrough case 2: - min = hourMinSec[1] + minute = hourMinSec[1] fallthrough case 1: hour = hourMinSec[0] @@ -188,5 +193,5 @@ func newTime(year int, month time.Month, day int, hourMinSec ...int) time.Time { panic("too many arguments") } - return time.Date(year, month, day, hour, min, sec, 0, time.UTC) + return time.Date(year, month, day, hour, minute, sec, 0, time.UTC) }