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.
		
		
		
		
		
			
		
			
				
					217 lines
				
				7.4 KiB
			
		
		
			
		
	
	
					217 lines
				
				7.4 KiB
			| 
											3 years ago
										 | var WritableStream = require('stream').Writable | ||
|  |                      || require('readable-stream').Writable, | ||
|  |     inherits = require('util').inherits, | ||
|  |     inspect = require('util').inspect; | ||
|  | 
 | ||
|  | var XRegExp = require('xregexp').XRegExp; | ||
|  | 
 | ||
|  | 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>.+)$'), | ||
|  |     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>.+)$'), | ||
|  |     RE_ENTRY_TOTAL = /^total/, | ||
|  |     RE_RES_END = /(?:^|\r?\n)(\d{3}) [^\r\n]*\r?\n/, | ||
|  |     RE_EOL = /\r?\n/g, | ||
|  |     RE_DASH = /\-/g; | ||
|  | 
 | ||
|  | var MONTHS = { | ||
|  |       jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, | ||
|  |       jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12 | ||
|  |     }; | ||
|  | 
 | ||
|  | function Parser(options) { | ||
|  |   if (!(this instanceof Parser)) | ||
|  |     return new Parser(options); | ||
|  |   WritableStream.call(this); | ||
|  | 
 | ||
|  |   this._buffer = ''; | ||
|  |   this._debug = options.debug; | ||
|  | } | ||
|  | inherits(Parser, WritableStream); | ||
|  | 
 | ||
|  | Parser.prototype._write = function(chunk, encoding, cb) { | ||
|  |   var m, code, reRmLeadCode, rest = '', debug = this._debug; | ||
|  | 
 | ||
|  |   this._buffer += chunk.toString('binary'); | ||
|  | 
 | ||
|  |   while (m = RE_RES_END.exec(this._buffer)) { | ||
|  |     // support multiple terminating responses in the buffer
 | ||
|  |     rest = this._buffer.substring(m.index + m[0].length); | ||
|  |     if (rest.length) | ||
|  |       this._buffer = this._buffer.substring(0, m.index + m[0].length); | ||
|  | 
 | ||
|  |     debug&&debug('[parser] < ' + inspect(this._buffer)); | ||
|  | 
 | ||
|  |     // we have a terminating response line
 | ||
|  |     code = parseInt(m[1], 10); | ||
|  | 
 | ||
|  |     // RFC 959 does not require each line in a multi-line response to begin
 | ||
|  |     // with '<code>-', but many servers will do this.
 | ||
|  |     //
 | ||
|  |     // remove this leading '<code>-' (or '<code> ' from last line) from each
 | ||
|  |     // line in the response ...
 | ||
|  |     reRmLeadCode = '(^|\\r?\\n)'; | ||
|  |     reRmLeadCode += m[1]; | ||
|  |     reRmLeadCode += '(?: |\\-)'; | ||
|  |     reRmLeadCode = new RegExp(reRmLeadCode, 'g'); | ||
|  |     var text = this._buffer.replace(reRmLeadCode, '$1').trim(); | ||
|  |     this._buffer = rest; | ||
|  | 
 | ||
|  |     debug&&debug('[parser] Response: code=' + code + ', buffer=' + inspect(text)); | ||
|  |     this.emit('response', code, text); | ||
|  |   } | ||
|  | 
 | ||
|  |   cb(); | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.parseFeat = function(text) { | ||
|  |   var lines = text.split(RE_EOL); | ||
|  |   lines.shift(); // initial response line
 | ||
|  |   lines.pop(); // final response line
 | ||
|  | 
 | ||
|  |   for (var i = 0, len = lines.length; i < len; ++i) | ||
|  |     lines[i] = lines[i].trim(); | ||
|  | 
 | ||
|  |   // just return the raw lines for now
 | ||
|  |   return lines; | ||
|  | }; | ||
|  | 
 | ||
|  | Parser.parseListEntry = function(line) { | ||
|  |   var ret, | ||
|  |       info, | ||
|  |       month, day, year, | ||
|  |       hour, mins; | ||
|  | 
 | ||
|  |   if (ret = XRegExp.exec(line, REX_LISTUNIX)) { | ||
|  |     info = { | ||
|  |       type: ret.type, | ||
|  |       name: undefined, | ||
|  |       target: undefined, | ||
|  |       sticky: false, | ||
|  |       rights: { | ||
|  |         user: ret.permission.substr(0, 3).replace(RE_DASH, ''), | ||
|  |         group: ret.permission.substr(3, 3).replace(RE_DASH, ''), | ||
|  |         other: ret.permission.substr(6, 3).replace(RE_DASH, '') | ||
|  |       }, | ||
|  |       acl: (ret.acl === '+'), | ||
|  |       owner: ret.owner, | ||
|  |       group: ret.group, | ||
|  |       size: parseInt(ret.size, 10), | ||
|  |       date: undefined | ||
|  |     }; | ||
|  | 
 | ||
|  |     // check for sticky bit
 | ||
|  |     var lastbit = info.rights.other.slice(-1); | ||
|  |     if (lastbit === 't') { | ||
|  |       info.rights.other = info.rights.other.slice(0, -1) + 'x'; | ||
|  |       info.sticky = true; | ||
|  |     } else if (lastbit === 'T') { | ||
|  |       info.rights.other = info.rights.other.slice(0, -1); | ||
|  |       info.sticky = true; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (ret.month1 !== undefined) { | ||
|  |       month = parseInt(MONTHS[ret.month1.toLowerCase()], 10); | ||
|  |       day = parseInt(ret.date1, 10); | ||
|  |       year = (new Date()).getFullYear(); | ||
|  |       hour = parseInt(ret.hour, 10); | ||
|  |       mins = parseInt(ret.minute, 10); | ||
|  |       if (month < 10) | ||
|  |         month = '0' + month; | ||
|  |       if (day < 10) | ||
|  |         day = '0' + day; | ||
|  |       if (hour < 10) | ||
|  |         hour = '0' + hour; | ||
|  |       if (mins < 10) | ||
|  |         mins = '0' + mins; | ||
|  |       info.date = new Date(year + '-' | ||
|  |                            + month + '-' | ||
|  |                            + day + 'T' | ||
|  |                            + hour + ':' | ||
|  |                            + mins); | ||
|  |       // If the date is in the past but no more than 6 months old, year
 | ||
|  |       // isn't displayed and doesn't have to be the current year.
 | ||
|  |       // 
 | ||
|  |       // If the date is in the future (less than an hour from now), year
 | ||
|  |       // isn't displayed and doesn't have to be the current year.
 | ||
|  |       // That second case is much more rare than the first and less annoying.
 | ||
|  |       // It's impossible to fix without knowing about the server's timezone,
 | ||
|  |       // so we just don't do anything about it.
 | ||
|  |       // 
 | ||
|  |       // If we're here with a time that is more than 28 hours into the
 | ||
|  |       // future (1 hour + maximum timezone offset which is 27 hours),
 | ||
|  |       // there is a problem -- we should be in the second conditional block
 | ||
|  |       if (info.date.getTime() - Date.now() > 100800000) { | ||
|  |         info.date = new Date((year - 1) + '-' | ||
|  |                              + month + '-' | ||
|  |                              + day + 'T' | ||
|  |                              + hour + ':' | ||
|  |                              + mins); | ||
|  |       } | ||
|  | 
 | ||
|  |       // If we're here with a time that is more than 6 months old, there's
 | ||
|  |       // a problem as well.
 | ||
|  |       // Maybe local & remote servers aren't on the same timezone (with remote
 | ||
|  |       // ahead of local)
 | ||
|  |       // For instance, remote is in 2014 while local is still in 2013. In
 | ||
|  |       // this case, a date like 01/01/13 02:23 could be detected instead of
 | ||
|  |       // 01/01/14 02:23 
 | ||
|  |       // Our trigger point will be 3600*24*31*6 (since we already use 31
 | ||
|  |       // as an upper bound, no need to add the 27 hours timezone offset)
 | ||
|  |       if (Date.now() - info.date.getTime() > 16070400000) { | ||
|  |         info.date = new Date((year + 1) + '-' | ||
|  |                              + month + '-' | ||
|  |                              + day + 'T' | ||
|  |                              + hour + ':' | ||
|  |                              + mins); | ||
|  |       } | ||
|  |     } else if (ret.month2 !== undefined) { | ||
|  |       month = parseInt(MONTHS[ret.month2.toLowerCase()], 10); | ||
|  |       day = parseInt(ret.date2, 10); | ||
|  |       year = parseInt(ret.year, 10); | ||
|  |       if (month < 10) | ||
|  |         month = '0' + month; | ||
|  |       if (day < 10) | ||
|  |         day = '0' + day; | ||
|  |       info.date = new Date(year + '-' + month + '-' + day); | ||
|  |     } | ||
|  |     if (ret.type === 'l') { | ||
|  |       var pos = ret.name.indexOf(' -> '); | ||
|  |       info.name = ret.name.substring(0, pos); | ||
|  |       info.target = ret.name.substring(pos+4); | ||
|  |     } else | ||
|  |       info.name = ret.name; | ||
|  |     ret = info; | ||
|  |   } else if (ret = XRegExp.exec(line, REX_LISTMSDOS)) { | ||
|  |     info = { | ||
|  |       name: ret.name, | ||
|  |       type: (ret.isdir ? 'd' : '-'), | ||
|  |       size: (ret.isdir ? 0 : parseInt(ret.size, 10)), | ||
|  |       date: undefined, | ||
|  |     }; | ||
|  |     month = parseInt(ret.month, 10), | ||
|  |     day = parseInt(ret.date, 10), | ||
|  |     year = parseInt(ret.year, 10), | ||
|  |     hour = parseInt(ret.hour, 10), | ||
|  |     mins = parseInt(ret.minute, 10); | ||
|  | 
 | ||
|  |     if (year < 70) | ||
|  |       year += 2000; | ||
|  |     else | ||
|  |       year += 1900; | ||
|  | 
 | ||
|  |     if (ret.ampm[0].toLowerCase() === 'p' && hour < 12) | ||
|  |       hour += 12; | ||
|  |     else if (ret.ampm[0].toLowerCase() === 'a' && hour === 12) | ||
|  |       hour = 0; | ||
|  | 
 | ||
|  |     info.date = new Date(year, month - 1, day, hour, mins); | ||
|  | 
 | ||
|  |     ret = info; | ||
|  |   } else if (!RE_ENTRY_TOTAL.test(line)) | ||
|  |     ret = line; // could not parse, so at least give the end user a chance to
 | ||
|  |                 // look at the raw listing themselves
 | ||
|  | 
 | ||
|  |   return ret; | ||
|  | }; | ||
|  | 
 | ||
|  | module.exports = Parser; |