parse.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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. // Split link name and target
  117. if i := strings.Index(e.Name, " -> "); i > 0 {
  118. e.Target = e.Name[i+4:]
  119. e.Name = e.Name[:i]
  120. }
  121. default:
  122. return nil, errUnknownListEntryType
  123. }
  124. if err := e.setTime(fields[5:8], now, loc); err != nil {
  125. return nil, err
  126. }
  127. return e, nil
  128. }
  129. // parseDirListLine parses a directory line in a format based on the output of
  130. // the MS-DOS DIR command.
  131. func parseDirListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  132. e := &Entry{}
  133. var err error
  134. // Try various time formats that DIR might use, and stop when one works.
  135. for _, format := range dirTimeFormats {
  136. if len(line) > len(format) {
  137. e.Time, err = time.ParseInLocation(format, line[:len(format)], loc)
  138. if err == nil {
  139. line = line[len(format):]
  140. break
  141. }
  142. }
  143. }
  144. if err != nil {
  145. // None of the time formats worked.
  146. return nil, errUnsupportedListLine
  147. }
  148. line = strings.TrimLeft(line, " ")
  149. if strings.HasPrefix(line, "<DIR>") {
  150. e.Type = EntryTypeFolder
  151. line = strings.TrimPrefix(line, "<DIR>")
  152. } else {
  153. space := strings.Index(line, " ")
  154. if space == -1 {
  155. return nil, errUnsupportedListLine
  156. }
  157. e.Size, err = strconv.ParseUint(line[:space], 10, 64)
  158. if err != nil {
  159. return nil, errUnsupportedListLine
  160. }
  161. e.Type = EntryTypeFile
  162. line = line[space:]
  163. }
  164. e.Name = strings.TrimLeft(line, " ")
  165. return e, nil
  166. }
  167. // parseHostedFTPLine parses a directory line in the non-standard format used
  168. // by hostedftp.com
  169. // -r-------- 0 user group 65222236 Feb 24 00:39 UABlacklistingWeek8.csv
  170. // (The link count is inexplicably 0)
  171. func parseHostedFTPLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  172. // Has the first field a length of 10 bytes?
  173. if strings.IndexByte(line, ' ') != 10 {
  174. return nil, errUnsupportedListLine
  175. }
  176. scanner := newScanner(line)
  177. fields := scanner.NextFields(2)
  178. if len(fields) < 2 || fields[1] != "0" {
  179. return nil, errUnsupportedListLine
  180. }
  181. // Set link count to 1 and attempt to parse as Unix.
  182. return parseLsListLine(fields[0]+" 1 "+scanner.Remaining(), now, loc)
  183. }
  184. // parseListLine parses the various non-standard format returned by the LIST
  185. // FTP command.
  186. func parseListLine(line string, now time.Time, loc *time.Location) (*Entry, error) {
  187. for _, f := range listLineParsers {
  188. e, err := f(line, now, loc)
  189. if err != errUnsupportedListLine {
  190. return e, err
  191. }
  192. }
  193. return nil, errUnsupportedListLine
  194. }
  195. func (e *Entry) setSize(str string) (err error) {
  196. e.Size, err = strconv.ParseUint(str, 0, 64)
  197. return
  198. }
  199. func (e *Entry) setTime(fields []string, now time.Time, loc *time.Location) (err error) {
  200. if strings.Contains(fields[2], ":") { // contains time
  201. thisYear, _, _ := now.Date()
  202. timeStr := fmt.Sprintf("%s %s %d %s", fields[1], fields[0], thisYear, fields[2])
  203. e.Time, err = time.ParseInLocation("_2 Jan 2006 15:04", timeStr, loc)
  204. /*
  205. On unix, `info ls` shows:
  206. 10.1.6 Formatting file timestamps
  207. ---------------------------------
  208. A timestamp is considered to be “recent” if it is less than six
  209. months old, and is not dated in the future. If a timestamp dated today
  210. is not listed in recent form, the timestamp is in the future, which
  211. means you probably have clock skew problems which may break programs
  212. like ‘make’ that rely on file timestamps.
  213. */
  214. if !e.Time.Before(now.AddDate(0, 6, 0)) {
  215. e.Time = e.Time.AddDate(-1, 0, 0)
  216. }
  217. } else { // only the date
  218. if len(fields[2]) != 4 {
  219. return errUnsupportedListDate
  220. }
  221. timeStr := fmt.Sprintf("%s %s %s 00:00", fields[1], fields[0], fields[2])
  222. e.Time, err = time.ParseInLocation("_2 Jan 2006 15:04", timeStr, loc)
  223. }
  224. return
  225. }