processor.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // synchronous utility for filtering entries and calculating subwalks
  2. import { GLOBSTAR } from 'minimatch';
  3. /**
  4. * A cache of which patterns have been processed for a given Path
  5. */
  6. export class HasWalkedCache {
  7. store;
  8. constructor(store = new Map()) {
  9. this.store = store;
  10. }
  11. copy() {
  12. return new HasWalkedCache(new Map(this.store));
  13. }
  14. hasWalked(target, pattern) {
  15. return this.store.get(target.fullpath())?.has(pattern.globString());
  16. }
  17. storeWalked(target, pattern) {
  18. const fullpath = target.fullpath();
  19. const cached = this.store.get(fullpath);
  20. if (cached)
  21. cached.add(pattern.globString());
  22. else
  23. this.store.set(fullpath, new Set([pattern.globString()]));
  24. }
  25. }
  26. /**
  27. * A record of which paths have been matched in a given walk step,
  28. * and whether they only are considered a match if they are a directory,
  29. * and whether their absolute or relative path should be returned.
  30. */
  31. export class MatchRecord {
  32. store = new Map();
  33. add(target, absolute, ifDir) {
  34. const n = (absolute ? 2 : 0) | (ifDir ? 1 : 0);
  35. const current = this.store.get(target);
  36. this.store.set(target, current === undefined ? n : n & current);
  37. }
  38. // match, absolute, ifdir
  39. entries() {
  40. return [...this.store.entries()].map(([path, n]) => [
  41. path,
  42. !!(n & 2),
  43. !!(n & 1),
  44. ]);
  45. }
  46. }
  47. /**
  48. * A collection of patterns that must be processed in a subsequent step
  49. * for a given path.
  50. */
  51. export class SubWalks {
  52. store = new Map();
  53. add(target, pattern) {
  54. if (!target.canReaddir()) {
  55. return;
  56. }
  57. const subs = this.store.get(target);
  58. if (subs) {
  59. if (!subs.find(p => p.globString() === pattern.globString())) {
  60. subs.push(pattern);
  61. }
  62. }
  63. else
  64. this.store.set(target, [pattern]);
  65. }
  66. get(target) {
  67. const subs = this.store.get(target);
  68. /* c8 ignore start */
  69. if (!subs) {
  70. throw new Error('attempting to walk unknown path');
  71. }
  72. /* c8 ignore stop */
  73. return subs;
  74. }
  75. entries() {
  76. return this.keys().map(k => [k, this.store.get(k)]);
  77. }
  78. keys() {
  79. return [...this.store.keys()].filter(t => t.canReaddir());
  80. }
  81. }
  82. /**
  83. * The class that processes patterns for a given path.
  84. *
  85. * Handles child entry filtering, and determining whether a path's
  86. * directory contents must be read.
  87. */
  88. export class Processor {
  89. hasWalkedCache;
  90. matches = new MatchRecord();
  91. subwalks = new SubWalks();
  92. patterns;
  93. follow;
  94. dot;
  95. opts;
  96. constructor(opts, hasWalkedCache) {
  97. this.opts = opts;
  98. this.follow = !!opts.follow;
  99. this.dot = !!opts.dot;
  100. this.hasWalkedCache = hasWalkedCache
  101. ? hasWalkedCache.copy()
  102. : new HasWalkedCache();
  103. }
  104. processPatterns(target, patterns) {
  105. this.patterns = patterns;
  106. const processingSet = patterns.map(p => [target, p]);
  107. // map of paths to the magic-starting subwalks they need to walk
  108. // first item in patterns is the filter
  109. for (let [t, pattern] of processingSet) {
  110. this.hasWalkedCache.storeWalked(t, pattern);
  111. const root = pattern.root();
  112. const absolute = pattern.isAbsolute();
  113. // start absolute patterns at root
  114. if (root) {
  115. t = t.resolve(root);
  116. const rest = pattern.rest();
  117. if (!rest) {
  118. this.matches.add(t, true, false);
  119. continue;
  120. }
  121. else {
  122. pattern = rest;
  123. }
  124. }
  125. let p;
  126. let rest;
  127. let changed = false;
  128. while (typeof (p = pattern.pattern()) === 'string' &&
  129. (rest = pattern.rest())) {
  130. const c = t.resolve(p);
  131. // we can be reasonably sure that .. is a readable dir
  132. if (c.isUnknown() && p !== '..')
  133. break;
  134. t = c;
  135. pattern = rest;
  136. changed = true;
  137. }
  138. p = pattern.pattern();
  139. rest = pattern.rest();
  140. if (changed) {
  141. if (this.hasWalkedCache.hasWalked(t, pattern))
  142. continue;
  143. this.hasWalkedCache.storeWalked(t, pattern);
  144. }
  145. // now we have either a final string for a known entry,
  146. // more strings for an unknown entry,
  147. // or a pattern starting with magic, mounted on t.
  148. if (typeof p === 'string') {
  149. // must be final entry
  150. if (!rest) {
  151. const ifDir = p === '..' || p === '' || p === '.';
  152. this.matches.add(t.resolve(p), absolute, ifDir);
  153. }
  154. else {
  155. this.subwalks.add(t, pattern);
  156. }
  157. continue;
  158. }
  159. else if (p === GLOBSTAR) {
  160. // if no rest, match and subwalk pattern
  161. // if rest, process rest and subwalk pattern
  162. // if it's a symlink, but we didn't get here by way of a
  163. // globstar match (meaning it's the first time THIS globstar
  164. // has traversed a symlink), then we follow it. Otherwise, stop.
  165. if (!t.isSymbolicLink() ||
  166. this.follow ||
  167. pattern.checkFollowGlobstar()) {
  168. this.subwalks.add(t, pattern);
  169. }
  170. const rp = rest?.pattern();
  171. const rrest = rest?.rest();
  172. if (!rest || ((rp === '' || rp === '.') && !rrest)) {
  173. // only HAS to be a dir if it ends in **/ or **/.
  174. // but ending in ** will match files as well.
  175. this.matches.add(t, absolute, rp === '' || rp === '.');
  176. }
  177. else {
  178. if (rp === '..') {
  179. // this would mean you're matching **/.. at the fs root,
  180. // and no thanks, I'm not gonna test that specific case.
  181. /* c8 ignore start */
  182. const tp = t.parent || t;
  183. /* c8 ignore stop */
  184. if (!rrest)
  185. this.matches.add(tp, absolute, true);
  186. else if (!this.hasWalkedCache.hasWalked(tp, rrest)) {
  187. this.subwalks.add(tp, rrest);
  188. }
  189. }
  190. }
  191. }
  192. else if (p instanceof RegExp) {
  193. this.subwalks.add(t, pattern);
  194. }
  195. }
  196. return this;
  197. }
  198. subwalkTargets() {
  199. return this.subwalks.keys();
  200. }
  201. child() {
  202. return new Processor(this.opts, this.hasWalkedCache);
  203. }
  204. // return a new Processor containing the subwalks for each
  205. // child entry, and a set of matches, and
  206. // a hasWalkedCache that's a copy of this one
  207. // then we're going to call
  208. filterEntries(parent, entries) {
  209. const patterns = this.subwalks.get(parent);
  210. // put matches and entry walks into the results processor
  211. const results = this.child();
  212. for (const e of entries) {
  213. for (const pattern of patterns) {
  214. const absolute = pattern.isAbsolute();
  215. const p = pattern.pattern();
  216. const rest = pattern.rest();
  217. if (p === GLOBSTAR) {
  218. results.testGlobstar(e, pattern, rest, absolute);
  219. }
  220. else if (p instanceof RegExp) {
  221. results.testRegExp(e, p, rest, absolute);
  222. }
  223. else {
  224. results.testString(e, p, rest, absolute);
  225. }
  226. }
  227. }
  228. return results;
  229. }
  230. testGlobstar(e, pattern, rest, absolute) {
  231. if (this.dot || !e.name.startsWith('.')) {
  232. if (!pattern.hasMore()) {
  233. this.matches.add(e, absolute, false);
  234. }
  235. if (e.canReaddir()) {
  236. // if we're in follow mode or it's not a symlink, just keep
  237. // testing the same pattern. If there's more after the globstar,
  238. // then this symlink consumes the globstar. If not, then we can
  239. // follow at most ONE symlink along the way, so we mark it, which
  240. // also checks to ensure that it wasn't already marked.
  241. if (this.follow || !e.isSymbolicLink()) {
  242. this.subwalks.add(e, pattern);
  243. }
  244. else if (e.isSymbolicLink()) {
  245. if (rest && pattern.checkFollowGlobstar()) {
  246. this.subwalks.add(e, rest);
  247. }
  248. else if (pattern.markFollowGlobstar()) {
  249. this.subwalks.add(e, pattern);
  250. }
  251. }
  252. }
  253. }
  254. // if the NEXT thing matches this entry, then also add
  255. // the rest.
  256. if (rest) {
  257. const rp = rest.pattern();
  258. if (typeof rp === 'string' &&
  259. // dots and empty were handled already
  260. rp !== '..' &&
  261. rp !== '' &&
  262. rp !== '.') {
  263. this.testString(e, rp, rest.rest(), absolute);
  264. }
  265. else if (rp === '..') {
  266. /* c8 ignore start */
  267. const ep = e.parent || e;
  268. /* c8 ignore stop */
  269. this.subwalks.add(ep, rest);
  270. }
  271. else if (rp instanceof RegExp) {
  272. this.testRegExp(e, rp, rest.rest(), absolute);
  273. }
  274. }
  275. }
  276. testRegExp(e, p, rest, absolute) {
  277. if (!p.test(e.name))
  278. return;
  279. if (!rest) {
  280. this.matches.add(e, absolute, false);
  281. }
  282. else {
  283. this.subwalks.add(e, rest);
  284. }
  285. }
  286. testString(e, p, rest, absolute) {
  287. // should never happen?
  288. if (!e.isNamed(p))
  289. return;
  290. if (!rest) {
  291. this.matches.add(e, absolute, false);
  292. }
  293. else {
  294. this.subwalks.add(e, rest);
  295. }
  296. }
  297. }
  298. //# sourceMappingURL=processor.js.map