parse.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. package ftp
  2. import (
  3. "errors"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "time"
  8. )
  9. var errUnsupportedListLine = errors.New("unsupported LIST line")
  10. var errUnsupportedListDate = errors.New("unsupported LIST date")
  11. var errUnknownListEntryType = errors.New("unknown entry type")
  12. type parseFunc func(string, time.Time, *time.Location) (*Entry, error)
  13. var listLineParsers = []parseFunc{
  14. parseRFC3659ListLine,
  15. parseLsListLine,
  16. parseDirListLine,
  17. parseHostedFTPLine,
  18. }
  19. var dirTimeFormats = []string{
  20. "01-02-06 03:04PM",
  21. "2006-01-02 15:04",
  22. }
  23. // parseRFC3659ListLine parses the style of directory line defined in RFC 3659.
  24. func parseRFC3659ListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  25. iSemicolon := strings.Index(line, ";")
  26. iWhitespace := strings.Index(line, " ")
  27. if iSemicolon < 0 || iSemicolon > iWhitespace {
  28. return nil, errUnsupportedListLine
  29. }
  30. e := &Entry{
  31. Name: line[iWhitespace+1:],
  32. }
  33. for _, field := range strings.Split(line[:iWhitespace-1], ";") {
  34. i := strings.Index(field, "=")
  35. if i < 1 {
  36. return nil, errUnsupportedListLine
  37. }
  38. key := strings.ToLower(field[:i])
  39. value := field[i+1:]
  40. switch key {
  41. case "modify":
  42. var err error
  43. e.Time, err = time.ParseInLocation("20060102150405", value, loc)
  44. if err != nil {
  45. return nil, err
  46. }
  47. case "type":
  48. switch value {
  49. case "dir", "cdir", "pdir":
  50. e.Type = EntryTypeFolder
  51. case "file":
  52. e.Type = EntryTypeFile
  53. }
  54. case "size":
  55. e.setSize(value)
  56. }
  57. }
  58. return e, nil
  59. }
  60. // parseLsListLine parses a directory line in a format based on the output of
  61. // the UNIX ls command.
  62. func parseLsListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  63. // Has the first field a length of 10 bytes?
  64. if strings.IndexByte(line, ' ') != 10 {
  65. return nil, errUnsupportedListLine
  66. }
  67. scanner := newScanner(line)
  68. fields := scanner.NextFields(6)
  69. if len(fields) < 6 {
  70. return nil, errUnsupportedListLine
  71. }
  72. if fields[1] == "folder" && fields[2] == "0" {
  73. e := &Entry{
  74. Type: EntryTypeFolder,
  75. Name: scanner.Remaining(),
  76. }
  77. if err := e.setTime(fields[3:6], now, loc); err != nil {
  78. return nil, err
  79. }
  80. return e, nil
  81. }
  82. if fields[1] == "0" {
  83. fields = append(fields, scanner.Next())
  84. e := &Entry{
  85. Type: EntryTypeFile,
  86. Name: scanner.Remaining(),
  87. }
  88. if err := e.setSize(fields[2]); err != nil {
  89. return nil, errUnsupportedListLine
  90. }
  91. if err := e.setTime(fields[4:7], now, loc); err != nil {
  92. return nil, err
  93. }
  94. return e, nil
  95. }
  96. // Read two more fields
  97. fields = append(fields, scanner.NextFields(2)...)
  98. if len(fields) < 8 {
  99. return nil, errUnsupportedListLine
  100. }
  101. e := &Entry{
  102. Name: scanner.Remaining(),
  103. }
  104. switch fields[0][0] {
  105. case '-':
  106. e.Type = EntryTypeFile
  107. if err := e.setSize(fields[4]); err != nil {
  108. return nil, err
  109. }
  110. case 'd':
  111. e.Type = EntryTypeFolder
  112. case 'l':
  113. e.Type = EntryTypeLink
  114. default:
  115. return nil, errUnknownListEntryType
  116. }
  117. if err := e.setTime(fields[5:8], now, loc); err != nil {
  118. return nil, err
  119. }
  120. return e, nil
  121. }
  122. // parseDirListLine parses a directory line in a format based on the output of
  123. // the MS-DOS DIR command.
  124. func parseDirListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  125. e := &Entry{}
  126. var err error
  127. // Try various time formats that DIR might use, and stop when one works.
  128. for _, format := range dirTimeFormats {
  129. if len(line) > len(format) {
  130. e.Time, err = time.ParseInLocation(format, line[:len(format)], loc)
  131. if err == nil {
  132. line = line[len(format):]
  133. break
  134. }
  135. }
  136. }
  137. if err != nil {
  138. // None of the time formats worked.
  139. return nil, errUnsupportedListLine
  140. }
  141. line = strings.TrimLeft(line, " ")
  142. if strings.HasPrefix(line, "<DIR>") {
  143. e.Type = EntryTypeFolder
  144. line = strings.TrimPrefix(line, "<DIR>")
  145. } else {
  146. space := strings.Index(line, " ")
  147. if space == -1 {
  148. return nil, errUnsupportedListLine
  149. }
  150. e.Size, err = strconv.ParseUint(line[:space], 10, 64)
  151. if err != nil {
  152. return nil, errUnsupportedListLine
  153. }
  154. e.Type = EntryTypeFile
  155. line = line[space:]
  156. }
  157. e.Name = strings.TrimLeft(line, " ")
  158. return e, nil
  159. }
  160. // parseHostedFTPLine parses a directory line in the non-standard format used
  161. // by hostedftp.com
  162. // -r-------- 0 user group 65222236 Feb 24 00:39 UABlacklistingWeek8.csv
  163. // (The link count is inexplicably 0)
  164. func parseHostedFTPLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  165. // Has the first field a length of 10 bytes?
  166. if strings.IndexByte(line, ' ') != 10 {
  167. return nil, errUnsupportedListLine
  168. }
  169. scanner := newScanner(line)
  170. fields := scanner.NextFields(2)
  171. if len(fields) < 2 || fields[1] != "0" {
  172. return nil, errUnsupportedListLine
  173. }
  174. // Set link count to 1 and attempt to parse as Unix.
  175. return parseLsListLine(fields[0]+" 1 "+scanner.Remaining(), now, loc)
  176. }
  177. // parseListLine parses the various non-standard format returned by the LIST
  178. // FTP command.
  179. func parseListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  180. for _, f := range listLineParsers {
  181. e, err := f(line, now, loc)
  182. if err != errUnsupportedListLine {
  183. return e, err
  184. }
  185. }
  186. return nil, errUnsupportedListLine
  187. }
  188. func (e *Entry) setSize(str string) (err error) {
  189. e.Size, err = strconv.ParseUint(str, 0, 64)
  190. return
  191. }
  192. func (e *Entry) setTime(fields []string, now time.Time, loc *time.Location) (err error) {
  193. if strings.Contains(fields[2], ":") { // contains time
  194. thisYear, _, _ := now.Date()
  195. timeStr := fmt.Sprintf("%s %s %d %s", fields[1], fields[0], thisYear, fields[2])
  196. e.Time, err = time.ParseInLocation("_2 Jan 2006 15:04", timeStr, loc)
  197. /*
  198. On unix, `info ls` shows:
  199. 10.1.6 Formatting file timestamps
  200. ---------------------------------
  201. A timestamp is considered to be “recent” if it is less than six
  202. months old, and is not dated in the future. If a timestamp dated today
  203. is not listed in recent form, the timestamp is in the future, which
  204. means you probably have clock skew problems which may break programs
  205. like ‘make’ that rely on file timestamps.
  206. */
  207. if !e.Time.Before(now.AddDate(0, 6, 0)) {
  208. e.Time = e.Time.AddDate(-1, 0, 0)
  209. }
  210. } else { // only the date
  211. if len(fields[2]) != 4 {
  212. return errUnsupportedListDate
  213. }
  214. timeStr := fmt.Sprintf("%s %s %s 00:00", fields[1], fields[0], fields[2])
  215. e.Time, err = time.ParseInLocation("_2 Jan 2006 15:04", timeStr, loc)
  216. }
  217. return
  218. }