parse.go 6.8 KB

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