parse.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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 exactly 10 bytes
  64. // - or 10 bytes with an additional '+' character for indicating ACLs?
  65. // If not, return.
  66. if i := strings.IndexByte(line, ' '); !(i == 10 || (i == 11 && line[10] == '+')) {
  67. return nil, errUnsupportedListLine
  68. }
  69. scanner := newScanner(line)
  70. fields := scanner.NextFields(6)
  71. if len(fields) < 6 {
  72. return nil, errUnsupportedListLine
  73. }
  74. if fields[1] == "folder" && fields[2] == "0" {
  75. e := &Entry{
  76. Type: EntryTypeFolder,
  77. Name: scanner.Remaining(),
  78. }
  79. if err := e.setTime(fields[3:6], now, loc); err != nil {
  80. return nil, err
  81. }
  82. return e, nil
  83. }
  84. if fields[1] == "0" {
  85. fields = append(fields, scanner.Next())
  86. e := &Entry{
  87. Type: EntryTypeFile,
  88. Name: scanner.Remaining(),
  89. }
  90. if err := e.setSize(fields[2]); err != nil {
  91. return nil, errUnsupportedListLine
  92. }
  93. if err := e.setTime(fields[4:7], now, loc); err != nil {
  94. return nil, err
  95. }
  96. return e, nil
  97. }
  98. // Read two more fields
  99. fields = append(fields, scanner.NextFields(2)...)
  100. if len(fields) < 8 {
  101. return nil, errUnsupportedListLine
  102. }
  103. e := &Entry{
  104. Name: scanner.Remaining(),
  105. }
  106. switch fields[0][0] {
  107. case '-':
  108. e.Type = EntryTypeFile
  109. if err := e.setSize(fields[4]); err != nil {
  110. return nil, err
  111. }
  112. case 'd':
  113. e.Type = EntryTypeFolder
  114. case 'l':
  115. e.Type = EntryTypeLink
  116. default:
  117. return nil, errUnknownListEntryType
  118. }
  119. if err := e.setTime(fields[5:8], now, loc); err != nil {
  120. return nil, err
  121. }
  122. return e, nil
  123. }
  124. // parseDirListLine parses a directory line in a format based on the output of
  125. // the MS-DOS DIR command.
  126. func parseDirListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  127. e := &Entry{}
  128. var err error
  129. // Try various time formats that DIR might use, and stop when one works.
  130. for _, format := range dirTimeFormats {
  131. if len(line) > len(format) {
  132. e.Time, err = time.ParseInLocation(format, line[:len(format)], loc)
  133. if err == nil {
  134. line = line[len(format):]
  135. break
  136. }
  137. }
  138. }
  139. if err != nil {
  140. // None of the time formats worked.
  141. return nil, errUnsupportedListLine
  142. }
  143. line = strings.TrimLeft(line, " ")
  144. if strings.HasPrefix(line, "<DIR>") {
  145. e.Type = EntryTypeFolder
  146. line = strings.TrimPrefix(line, "<DIR>")
  147. } else {
  148. space := strings.Index(line, " ")
  149. if space == -1 {
  150. return nil, errUnsupportedListLine
  151. }
  152. e.Size, err = strconv.ParseUint(line[:space], 10, 64)
  153. if err != nil {
  154. return nil, errUnsupportedListLine
  155. }
  156. e.Type = EntryTypeFile
  157. line = line[space:]
  158. }
  159. e.Name = strings.TrimLeft(line, " ")
  160. return e, nil
  161. }
  162. // parseHostedFTPLine parses a directory line in the non-standard format used
  163. // by hostedftp.com
  164. // -r-------- 0 user group 65222236 Feb 24 00:39 UABlacklistingWeek8.csv
  165. // (The link count is inexplicably 0)
  166. func parseHostedFTPLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  167. // Has the first field a length of 10 bytes?
  168. if strings.IndexByte(line, ' ') != 10 {
  169. return nil, errUnsupportedListLine
  170. }
  171. scanner := newScanner(line)
  172. fields := scanner.NextFields(2)
  173. if len(fields) < 2 || fields[1] != "0" {
  174. return nil, errUnsupportedListLine
  175. }
  176. // Set link count to 1 and attempt to parse as Unix.
  177. return parseLsListLine(fields[0]+" 1 "+scanner.Remaining(), now, loc)
  178. }
  179. // parseListLine parses the various non-standard format returned by the LIST
  180. // FTP command.
  181. func parseListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  182. for _, f := range listLineParsers {
  183. e, err := f(line, now, loc)
  184. if err != errUnsupportedListLine {
  185. return e, err
  186. }
  187. }
  188. return nil, errUnsupportedListLine
  189. }
  190. func (e *Entry) setSize(str string) (err error) {
  191. e.Size, err = strconv.ParseUint(str, 0, 64)
  192. return
  193. }
  194. func (e *Entry) setTime(fields []string, now time.Time, loc *time.Location) (err error) {
  195. if strings.Contains(fields[2], ":") { // contains time
  196. thisYear, _, _ := now.Date()
  197. timeStr := fmt.Sprintf("%s %s %d %s", fields[1], fields[0], thisYear, fields[2])
  198. e.Time, err = time.ParseInLocation("_2 Jan 2006 15:04", timeStr, loc)
  199. /*
  200. On unix, `info ls` shows:
  201. 10.1.6 Formatting file timestamps
  202. ---------------------------------
  203. A timestamp is considered to be “recent” if it is less than six
  204. months old, and is not dated in the future. If a timestamp dated today
  205. is not listed in recent form, the timestamp is in the future, which
  206. means you probably have clock skew problems which may break programs
  207. like ‘make’ that rely on file timestamps.
  208. */
  209. if !e.Time.Before(now.AddDate(0, 6, 0)) {
  210. e.Time = e.Time.AddDate(-1, 0, 0)
  211. }
  212. } else { // only the date
  213. if len(fields[2]) != 4 {
  214. return errUnsupportedListDate
  215. }
  216. timeStr := fmt.Sprintf("%s %s %s 00:00", fields[1], fields[0], fields[2])
  217. e.Time, err = time.ParseInLocation("_2 Jan 2006 15:04", timeStr, loc)
  218. }
  219. return
  220. }