You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

parser.js 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. var WritableStream = require('stream').Writable
  2. || require('readable-stream').Writable,
  3. inherits = require('util').inherits,
  4. inspect = require('util').inspect;
  5. var XRegExp = require('xregexp').XRegExp;
  6. var REX_LISTUNIX = XRegExp.cache('^(?<type>[\\-ld])(?<permission>([\\-r][\\-w][\\-xstT]){3})(?<acl>(\\+))?\\s+(?<inodes>\\d+)\\s+(?<owner>\\S+)\\s+(?<group>\\S+)\\s+(?<size>\\d+)\\s+(?<timestamp>((?<month1>\\w{3})\\s+(?<date1>\\d{1,2})\\s+(?<hour>\\d{1,2}):(?<minute>\\d{2}))|((?<month2>\\w{3})\\s+(?<date2>\\d{1,2})\\s+(?<year>\\d{4})))\\s+(?<name>.+)$'),
  7. REX_LISTMSDOS = XRegExp.cache('^(?<month>\\d{2})(?:\\-|\\/)(?<date>\\d{2})(?:\\-|\\/)(?<year>\\d{2,4})\\s+(?<hour>\\d{2}):(?<minute>\\d{2})\\s{0,1}(?<ampm>[AaMmPp]{1,2})\\s+(?:(?<size>\\d+)|(?<isdir>\\<DIR\\>))\\s+(?<name>.+)$'),
  8. RE_ENTRY_TOTAL = /^total/,
  9. RE_RES_END = /(?:^|\r?\n)(\d{3}) [^\r\n]*\r?\n/,
  10. RE_EOL = /\r?\n/g,
  11. RE_DASH = /\-/g;
  12. var MONTHS = {
  13. jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
  14. jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12
  15. };
  16. function Parser(options) {
  17. if (!(this instanceof Parser))
  18. return new Parser(options);
  19. WritableStream.call(this);
  20. this._buffer = '';
  21. this._debug = options.debug;
  22. }
  23. inherits(Parser, WritableStream);
  24. Parser.prototype._write = function(chunk, encoding, cb) {
  25. var m, code, reRmLeadCode, rest = '', debug = this._debug;
  26. this._buffer += chunk.toString('binary');
  27. while (m = RE_RES_END.exec(this._buffer)) {
  28. // support multiple terminating responses in the buffer
  29. rest = this._buffer.substring(m.index + m[0].length);
  30. if (rest.length)
  31. this._buffer = this._buffer.substring(0, m.index + m[0].length);
  32. debug&&debug('[parser] < ' + inspect(this._buffer));
  33. // we have a terminating response line
  34. code = parseInt(m[1], 10);
  35. // RFC 959 does not require each line in a multi-line response to begin
  36. // with '<code>-', but many servers will do this.
  37. //
  38. // remove this leading '<code>-' (or '<code> ' from last line) from each
  39. // line in the response ...
  40. reRmLeadCode = '(^|\\r?\\n)';
  41. reRmLeadCode += m[1];
  42. reRmLeadCode += '(?: |\\-)';
  43. reRmLeadCode = new RegExp(reRmLeadCode, 'g');
  44. var text = this._buffer.replace(reRmLeadCode, '$1').trim();
  45. this._buffer = rest;
  46. debug&&debug('[parser] Response: code=' + code + ', buffer=' + inspect(text));
  47. this.emit('response', code, text);
  48. }
  49. cb();
  50. };
  51. Parser.parseFeat = function(text) {
  52. var lines = text.split(RE_EOL);
  53. lines.shift(); // initial response line
  54. lines.pop(); // final response line
  55. for (var i = 0, len = lines.length; i < len; ++i)
  56. lines[i] = lines[i].trim();
  57. // just return the raw lines for now
  58. return lines;
  59. };
  60. Parser.parseListEntry = function(line) {
  61. var ret,
  62. info,
  63. month, day, year,
  64. hour, mins;
  65. if (ret = XRegExp.exec(line, REX_LISTUNIX)) {
  66. info = {
  67. type: ret.type,
  68. name: undefined,
  69. target: undefined,
  70. sticky: false,
  71. rights: {
  72. user: ret.permission.substr(0, 3).replace(RE_DASH, ''),
  73. group: ret.permission.substr(3, 3).replace(RE_DASH, ''),
  74. other: ret.permission.substr(6, 3).replace(RE_DASH, '')
  75. },
  76. acl: (ret.acl === '+'),
  77. owner: ret.owner,
  78. group: ret.group,
  79. size: parseInt(ret.size, 10),
  80. date: undefined
  81. };
  82. // check for sticky bit
  83. var lastbit = info.rights.other.slice(-1);
  84. if (lastbit === 't') {
  85. info.rights.other = info.rights.other.slice(0, -1) + 'x';
  86. info.sticky = true;
  87. } else if (lastbit === 'T') {
  88. info.rights.other = info.rights.other.slice(0, -1);
  89. info.sticky = true;
  90. }
  91. if (ret.month1 !== undefined) {
  92. month = parseInt(MONTHS[ret.month1.toLowerCase()], 10);
  93. day = parseInt(ret.date1, 10);
  94. year = (new Date()).getFullYear();
  95. hour = parseInt(ret.hour, 10);
  96. mins = parseInt(ret.minute, 10);
  97. if (month < 10)
  98. month = '0' + month;
  99. if (day < 10)
  100. day = '0' + day;
  101. if (hour < 10)
  102. hour = '0' + hour;
  103. if (mins < 10)
  104. mins = '0' + mins;
  105. info.date = new Date(year + '-'
  106. + month + '-'
  107. + day + 'T'
  108. + hour + ':'
  109. + mins);
  110. // If the date is in the past but no more than 6 months old, year
  111. // isn't displayed and doesn't have to be the current year.
  112. //
  113. // If the date is in the future (less than an hour from now), year
  114. // isn't displayed and doesn't have to be the current year.
  115. // That second case is much more rare than the first and less annoying.
  116. // It's impossible to fix without knowing about the server's timezone,
  117. // so we just don't do anything about it.
  118. //
  119. // If we're here with a time that is more than 28 hours into the
  120. // future (1 hour + maximum timezone offset which is 27 hours),
  121. // there is a problem -- we should be in the second conditional block
  122. if (info.date.getTime() - Date.now() > 100800000) {
  123. info.date = new Date((year - 1) + '-'
  124. + month + '-'
  125. + day + 'T'
  126. + hour + ':'
  127. + mins);
  128. }
  129. // If we're here with a time that is more than 6 months old, there's
  130. // a problem as well.
  131. // Maybe local & remote servers aren't on the same timezone (with remote
  132. // ahead of local)
  133. // For instance, remote is in 2014 while local is still in 2013. In
  134. // this case, a date like 01/01/13 02:23 could be detected instead of
  135. // 01/01/14 02:23
  136. // Our trigger point will be 3600*24*31*6 (since we already use 31
  137. // as an upper bound, no need to add the 27 hours timezone offset)
  138. if (Date.now() - info.date.getTime() > 16070400000) {
  139. info.date = new Date((year + 1) + '-'
  140. + month + '-'
  141. + day + 'T'
  142. + hour + ':'
  143. + mins);
  144. }
  145. } else if (ret.month2 !== undefined) {
  146. month = parseInt(MONTHS[ret.month2.toLowerCase()], 10);
  147. day = parseInt(ret.date2, 10);
  148. year = parseInt(ret.year, 10);
  149. if (month < 10)
  150. month = '0' + month;
  151. if (day < 10)
  152. day = '0' + day;
  153. info.date = new Date(year + '-' + month + '-' + day);
  154. }
  155. if (ret.type === 'l') {
  156. var pos = ret.name.indexOf(' -> ');
  157. info.name = ret.name.substring(0, pos);
  158. info.target = ret.name.substring(pos+4);
  159. } else
  160. info.name = ret.name;
  161. ret = info;
  162. } else if (ret = XRegExp.exec(line, REX_LISTMSDOS)) {
  163. info = {
  164. name: ret.name,
  165. type: (ret.isdir ? 'd' : '-'),
  166. size: (ret.isdir ? 0 : parseInt(ret.size, 10)),
  167. date: undefined,
  168. };
  169. month = parseInt(ret.month, 10),
  170. day = parseInt(ret.date, 10),
  171. year = parseInt(ret.year, 10),
  172. hour = parseInt(ret.hour, 10),
  173. mins = parseInt(ret.minute, 10);
  174. if (year < 70)
  175. year += 2000;
  176. else
  177. year += 1900;
  178. if (ret.ampm[0].toLowerCase() === 'p' && hour < 12)
  179. hour += 12;
  180. else if (ret.ampm[0].toLowerCase() === 'a' && hour === 12)
  181. hour = 0;
  182. info.date = new Date(year, month - 1, day, hour, mins);
  183. ret = info;
  184. } else if (!RE_ENTRY_TOTAL.test(line))
  185. ret = line; // could not parse, so at least give the end user a chance to
  186. // look at the raw listing themselves
  187. return ret;
  188. };
  189. module.exports = Parser;