package ftp import ( "errors" "fmt" "strconv" "strings" "time" ) var errUnsupportedListLine = errors.New("unsupported LIST line") var errUnsupportedListDate = errors.New("unsupported LIST date") var errUnknownListEntryType = errors.New("unknown entry type") type parseFunc func(string, time.Time, *time.Location) (*Entry, error) var listLineParsers = []parseFunc{ parseRFC3659ListLine, parseLsListLine, parseDirListLine, parseHostedFTPLine, } var dirTimeFormats = []string{ "01-02-06 03:04PM", "2006-01-02 15:04", } // parseRFC3659ListLine parses the style of directory line defined in RFC 3659. func parseRFC3659ListLine(line string, now time.Time, loc *time.Location) (*Entry, error) { iSemicolon := strings.Index(line, ";") iWhitespace := strings.Index(line, " ") if iSemicolon < 0 || iSemicolon > iWhitespace { return nil, errUnsupportedListLine } e := &Entry{ Name: line[iWhitespace+1:], } for _, field := range strings.Split(line[:iWhitespace-1], ";") { i := strings.Index(field, "=") if i < 1 { return nil, errUnsupportedListLine } key := strings.ToLower(field[:i]) value := field[i+1:] switch key { case "modify": var err error e.Time, err = time.ParseInLocation("20060102150405", value, loc) if err != nil { return nil, err } case "type": switch value { case "dir", "cdir", "pdir": e.Type = EntryTypeFolder case "file": e.Type = EntryTypeFile } case "size": e.setSize(value) } } return e, nil } // parseLsListLine parses a directory line in a format based on the output of // the UNIX ls command. func parseLsListLine(line string, now time.Time, loc *time.Location) (*Entry, error) { // Has the first field a length of exactly 10 bytes // - or 10 bytes with an additional '+' character for indicating ACLs? // If not, return. if i := strings.IndexByte(line, ' '); !(i == 10 || (i == 11 && line[10] == '+')) { return nil, errUnsupportedListLine } scanner := newScanner(line) fields := scanner.NextFields(6) if len(fields) < 6 { return nil, errUnsupportedListLine } if fields[1] == "folder" && fields[2] == "0" { e := &Entry{ Type: EntryTypeFolder, Name: scanner.Remaining(), } if err := e.setTime(fields[3:6], now, loc); err != nil { return nil, err } return e, nil } if fields[1] == "0" { fields = append(fields, scanner.Next()) e := &Entry{ Type: EntryTypeFile, Name: scanner.Remaining(), } if err := e.setSize(fields[2]); err != nil { return nil, errUnsupportedListLine } if err := e.setTime(fields[4:7], now, loc); err != nil { return nil, err } return e, nil } // Read two more fields fields = append(fields, scanner.NextFields(2)...) if len(fields) < 8 { return nil, errUnsupportedListLine } e := &Entry{ Name: scanner.Remaining(), } switch fields[0][0] { case '-': e.Type = EntryTypeFile if err := e.setSize(fields[4]); err != nil { return nil, err } case 'd': e.Type = EntryTypeFolder case 'l': e.Type = EntryTypeLink // Split link name and target if i := strings.Index(e.Name, " -> "); i > 0 { e.Target = e.Name[i+4:] e.Name = e.Name[:i] } default: return nil, errUnknownListEntryType } if err := e.setTime(fields[5:8], now, loc); err != nil { return nil, err } return e, nil } // 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) { e := &Entry{} var err error // Try various time formats that DIR might use, and stop when one works. for _, format := range dirTimeFormats { if len(line) > len(format) { e.Time, err = time.ParseInLocation(format, line[:len(format)], loc) if err == nil { line = line[len(format):] break } } } if err != nil { // None of the time formats worked. return nil, errUnsupportedListLine } line = strings.TrimLeft(line, " ") if strings.HasPrefix(line, "