parseListMLSD.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.parseMLSxDate = exports.transformList = exports.parseLine = exports.testLine = void 0;
  4. const FileInfo_1 = require("./FileInfo");
  5. function parseSize(value, info) {
  6. info.size = parseInt(value, 10);
  7. }
  8. /**
  9. * Parsers for MLSD facts.
  10. */
  11. const factHandlersByName = {
  12. "size": parseSize, // File size
  13. "sizd": parseSize, // Directory size
  14. "unique": (value, info) => {
  15. info.uniqueID = value;
  16. },
  17. "modify": (value, info) => {
  18. info.modifiedAt = parseMLSxDate(value);
  19. info.rawModifiedAt = info.modifiedAt.toISOString();
  20. },
  21. "type": (value, info) => {
  22. // There seems to be confusion on how to handle symbolic links for Unix. RFC 3659 doesn't describe
  23. // this but mentions some examples using the syntax `type=OS.unix=slink:<target>`. But according to
  24. // an entry in the Errata (https://www.rfc-editor.org/errata/eid1500) this syntax can't be valid.
  25. // Instead it proposes to use `type=OS.unix=symlink` and to then list the actual target of the
  26. // symbolic link as another entry in the directory listing. The unique identifiers can then be used
  27. // to derive the connection between link(s) and target. We'll have to handle both cases as there
  28. // are differing opinions on how to deal with this. Here are some links on this topic:
  29. // - ProFTPD source: https://github.com/proftpd/proftpd/blob/56e6dfa598cbd4ef5c6cba439bcbcd53a63e3b21/modules/mod_facts.c#L531
  30. // - ProFTPD bug: http://bugs.proftpd.org/show_bug.cgi?id=3318
  31. // - ProFTPD statement: http://www.proftpd.org/docs/modules/mod_facts.html
  32. // – FileZilla bug: https://trac.filezilla-project.org/ticket/9310
  33. if (value.startsWith("OS.unix=slink")) {
  34. info.type = FileInfo_1.FileType.SymbolicLink;
  35. info.link = value.substr(value.indexOf(":") + 1);
  36. return 1 /* FactHandlerResult.Continue */;
  37. }
  38. switch (value) {
  39. case "file":
  40. info.type = FileInfo_1.FileType.File;
  41. break;
  42. case "dir":
  43. info.type = FileInfo_1.FileType.Directory;
  44. break;
  45. case "OS.unix=symlink":
  46. info.type = FileInfo_1.FileType.SymbolicLink;
  47. // The target of the symbolic link might be defined in another line in the directory listing.
  48. // We'll handle this in `transformList()` below.
  49. break;
  50. case "cdir": // Current directory being listed
  51. case "pdir": // Parent directory
  52. return 2 /* FactHandlerResult.IgnoreFile */; // Don't include these entries in the listing
  53. default:
  54. info.type = FileInfo_1.FileType.Unknown;
  55. }
  56. return 1 /* FactHandlerResult.Continue */;
  57. },
  58. "unix.mode": (value, info) => {
  59. const digits = value.substr(-3);
  60. info.permissions = {
  61. user: parseInt(digits[0], 10),
  62. group: parseInt(digits[1], 10),
  63. world: parseInt(digits[2], 10)
  64. };
  65. },
  66. "unix.ownername": (value, info) => {
  67. info.user = value;
  68. },
  69. "unix.owner": (value, info) => {
  70. if (info.user === undefined)
  71. info.user = value;
  72. },
  73. get "unix.uid"() {
  74. return this["unix.owner"];
  75. },
  76. "unix.groupname": (value, info) => {
  77. info.group = value;
  78. },
  79. "unix.group": (value, info) => {
  80. if (info.group === undefined)
  81. info.group = value;
  82. },
  83. get "unix.gid"() {
  84. return this["unix.group"];
  85. }
  86. // Regarding the fact "perm":
  87. // We don't handle permission information stored in "perm" because its information is conceptually
  88. // different from what users of FTP clients usually associate with "permissions". Those that have
  89. // some expectations (and probably want to edit them with a SITE command) often unknowingly expect
  90. // the Unix permission system. The information passed by "perm" describes what FTP commands can be
  91. // executed with a file/directory. But even this can be either incomplete or just meant as a "guide"
  92. // as the spec mentions. From https://tools.ietf.org/html/rfc3659#section-7.5.5: "The permissions are
  93. // described here as they apply to FTP commands. They may not map easily into particular permissions
  94. // available on the server's operating system." The parser by Apache Commons tries to translate these
  95. // to Unix permissions – this is misleading users and might not even be correct.
  96. };
  97. /**
  98. * Split a string once at the first position of a delimiter. For example
  99. * `splitStringOnce("a b c d", " ")` returns `["a", "b c d"]`.
  100. */
  101. function splitStringOnce(str, delimiter) {
  102. const pos = str.indexOf(delimiter);
  103. const a = str.substr(0, pos);
  104. const b = str.substr(pos + delimiter.length);
  105. return [a, b];
  106. }
  107. /**
  108. * Returns true if a given line might be part of an MLSD listing.
  109. *
  110. * - Example 1: `size=15227;type=dir;perm=el;modify=20190419065730; test one`
  111. * - Example 2: ` file name` (leading space)
  112. */
  113. function testLine(line) {
  114. return /^\S+=\S+;/.test(line) || line.startsWith(" ");
  115. }
  116. exports.testLine = testLine;
  117. /**
  118. * Parse single line as MLSD listing, see specification at https://tools.ietf.org/html/rfc3659#section-7.
  119. */
  120. function parseLine(line) {
  121. const [packedFacts, name] = splitStringOnce(line, " ");
  122. if (name === "" || name === "." || name === "..") {
  123. return undefined;
  124. }
  125. const info = new FileInfo_1.FileInfo(name);
  126. const facts = packedFacts.split(";");
  127. for (const fact of facts) {
  128. const [factName, factValue] = splitStringOnce(fact, "=");
  129. if (!factValue) {
  130. continue;
  131. }
  132. const factHandler = factHandlersByName[factName.toLowerCase()];
  133. if (!factHandler) {
  134. continue;
  135. }
  136. const result = factHandler(factValue, info);
  137. if (result === 2 /* FactHandlerResult.IgnoreFile */) {
  138. return undefined;
  139. }
  140. }
  141. return info;
  142. }
  143. exports.parseLine = parseLine;
  144. function transformList(files) {
  145. // Create a map of all files that are not symbolic links by their unique ID
  146. const nonLinksByID = new Map();
  147. for (const file of files) {
  148. if (!file.isSymbolicLink && file.uniqueID !== undefined) {
  149. nonLinksByID.set(file.uniqueID, file);
  150. }
  151. }
  152. const resolvedFiles = [];
  153. for (const file of files) {
  154. // Try to associate unresolved symbolic links with a target file/directory.
  155. if (file.isSymbolicLink && file.uniqueID !== undefined && file.link === undefined) {
  156. const target = nonLinksByID.get(file.uniqueID);
  157. if (target !== undefined) {
  158. file.link = target.name;
  159. }
  160. }
  161. // The target of a symbolic link is listed as an entry in the directory listing but might
  162. // have a path pointing outside of this directory. In that case we don't want this entry
  163. // to be part of the listing. We generally don't want these kind of entries at all.
  164. const isPartOfDirectory = !file.name.includes("/");
  165. if (isPartOfDirectory) {
  166. resolvedFiles.push(file);
  167. }
  168. }
  169. return resolvedFiles;
  170. }
  171. exports.transformList = transformList;
  172. /**
  173. * Parse date as specified in https://tools.ietf.org/html/rfc3659#section-2.3.
  174. *
  175. * Message contains response code and modified time in the format: YYYYMMDDHHMMSS[.sss]
  176. * For example `19991005213102` or `19980615100045.014`.
  177. */
  178. function parseMLSxDate(fact) {
  179. return new Date(Date.UTC(+fact.slice(0, 4), // Year
  180. +fact.slice(4, 6) - 1, // Month
  181. +fact.slice(6, 8), // Date
  182. +fact.slice(8, 10), // Hours
  183. +fact.slice(10, 12), // Minutes
  184. +fact.slice(12, 14), // Seconds
  185. +fact.slice(15, 18) // Milliseconds
  186. ));
  187. }
  188. exports.parseMLSxDate = parseMLSxDate;