1// synchronous utility for filtering entries and calculating subwalks 2import { GLOBSTAR } from 'minimatch'; 3/** 4 * A cache of which patterns have been processed for a given Path 5 */ 6export 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 */ 31export 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 */ 51export 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 */ 88export 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() && this.opts.absolute !== false; 113 // start absolute patterns at root 114 if (root) { 115 t = t.resolve(root === '/' && this.opts.root !== undefined 116 ? this.opts.root 117 : root); 118 const rest = pattern.rest(); 119 if (!rest) { 120 this.matches.add(t, true, false); 121 continue; 122 } 123 else { 124 pattern = rest; 125 } 126 } 127 if (t.isENOENT()) 128 continue; 129 let p; 130 let rest; 131 let changed = false; 132 while (typeof (p = pattern.pattern()) === 'string' && 133 (rest = pattern.rest())) { 134 const c = t.resolve(p); 135 // we can be reasonably sure that .. is a readable dir 136 if (c.isUnknown() && p !== '..') 137 break; 138 t = c; 139 pattern = rest; 140 changed = true; 141 } 142 p = pattern.pattern(); 143 rest = pattern.rest(); 144 if (changed) { 145 if (this.hasWalkedCache.hasWalked(t, pattern)) 146 continue; 147 this.hasWalkedCache.storeWalked(t, pattern); 148 } 149 // now we have either a final string for a known entry, 150 // more strings for an unknown entry, 151 // or a pattern starting with magic, mounted on t. 152 if (typeof p === 'string') { 153 // must be final entry 154 if (!rest) { 155 const ifDir = p === '..' || p === '' || p === '.'; 156 this.matches.add(t.resolve(p), absolute, ifDir); 157 } 158 else { 159 this.subwalks.add(t, pattern); 160 } 161 continue; 162 } 163 else if (p === GLOBSTAR) { 164 // if no rest, match and subwalk pattern 165 // if rest, process rest and subwalk pattern 166 // if it's a symlink, but we didn't get here by way of a 167 // globstar match (meaning it's the first time THIS globstar 168 // has traversed a symlink), then we follow it. Otherwise, stop. 169 if (!t.isSymbolicLink() || 170 this.follow || 171 pattern.checkFollowGlobstar()) { 172 this.subwalks.add(t, pattern); 173 } 174 const rp = rest?.pattern(); 175 const rrest = rest?.rest(); 176 if (!rest || ((rp === '' || rp === '.') && !rrest)) { 177 // only HAS to be a dir if it ends in **/ or **/. 178 // but ending in ** will match files as well. 179 this.matches.add(t, absolute, rp === '' || rp === '.'); 180 } 181 else { 182 if (rp === '..') { 183 // this would mean you're matching **/.. at the fs root, 184 // and no thanks, I'm not gonna test that specific case. 185 /* c8 ignore start */ 186 const tp = t.parent || t; 187 /* c8 ignore stop */ 188 if (!rrest) 189 this.matches.add(tp, absolute, true); 190 else if (!this.hasWalkedCache.hasWalked(tp, rrest)) { 191 this.subwalks.add(tp, rrest); 192 } 193 } 194 } 195 } 196 else if (p instanceof RegExp) { 197 this.subwalks.add(t, pattern); 198 } 199 } 200 return this; 201 } 202 subwalkTargets() { 203 return this.subwalks.keys(); 204 } 205 child() { 206 return new Processor(this.opts, this.hasWalkedCache); 207 } 208 // return a new Processor containing the subwalks for each 209 // child entry, and a set of matches, and 210 // a hasWalkedCache that's a copy of this one 211 // then we're going to call 212 filterEntries(parent, entries) { 213 const patterns = this.subwalks.get(parent); 214 // put matches and entry walks into the results processor 215 const results = this.child(); 216 for (const e of entries) { 217 for (const pattern of patterns) { 218 const absolute = pattern.isAbsolute(); 219 const p = pattern.pattern(); 220 const rest = pattern.rest(); 221 if (p === GLOBSTAR) { 222 results.testGlobstar(e, pattern, rest, absolute); 223 } 224 else if (p instanceof RegExp) { 225 results.testRegExp(e, p, rest, absolute); 226 } 227 else { 228 results.testString(e, p, rest, absolute); 229 } 230 } 231 } 232 return results; 233 } 234 testGlobstar(e, pattern, rest, absolute) { 235 if (this.dot || !e.name.startsWith('.')) { 236 if (!pattern.hasMore()) { 237 this.matches.add(e, absolute, false); 238 } 239 if (e.canReaddir()) { 240 // if we're in follow mode or it's not a symlink, just keep 241 // testing the same pattern. If there's more after the globstar, 242 // then this symlink consumes the globstar. If not, then we can 243 // follow at most ONE symlink along the way, so we mark it, which 244 // also checks to ensure that it wasn't already marked. 245 if (this.follow || !e.isSymbolicLink()) { 246 this.subwalks.add(e, pattern); 247 } 248 else if (e.isSymbolicLink()) { 249 if (rest && pattern.checkFollowGlobstar()) { 250 this.subwalks.add(e, rest); 251 } 252 else if (pattern.markFollowGlobstar()) { 253 this.subwalks.add(e, pattern); 254 } 255 } 256 } 257 } 258 // if the NEXT thing matches this entry, then also add 259 // the rest. 260 if (rest) { 261 const rp = rest.pattern(); 262 if (typeof rp === 'string' && 263 // dots and empty were handled already 264 rp !== '..' && 265 rp !== '' && 266 rp !== '.') { 267 this.testString(e, rp, rest.rest(), absolute); 268 } 269 else if (rp === '..') { 270 /* c8 ignore start */ 271 const ep = e.parent || e; 272 /* c8 ignore stop */ 273 this.subwalks.add(ep, rest); 274 } 275 else if (rp instanceof RegExp) { 276 this.testRegExp(e, rp, rest.rest(), absolute); 277 } 278 } 279 } 280 testRegExp(e, p, rest, absolute) { 281 if (!p.test(e.name)) 282 return; 283 if (!rest) { 284 this.matches.add(e, absolute, false); 285 } 286 else { 287 this.subwalks.add(e, rest); 288 } 289 } 290 testString(e, p, rest, absolute) { 291 // should never happen? 292 if (!e.isNamed(p)) 293 return; 294 if (!rest) { 295 this.matches.add(e, absolute, false); 296 } 297 else { 298 this.subwalks.add(e, rest); 299 } 300 } 301} 302//# sourceMappingURL=processor.js.map