// this is just a very light wrapper around 2 arrays with an offset index import type { ParseReturnFiltered } from 'minimatch' import { GLOBSTAR } from 'minimatch' /** * One portion of a parsed minimatch pattern: a string, a Matcher for * a portion with magic, or GLOBSTAR for `**`. */ export type MMPattern = ParseReturnFiltered /** The Matcher that minimatch makes for a portion with magic. */ export type MMMatcher = Exclude // an array of length >= 1 export type PatternList = [p: MMPattern, ...rest: MMPattern[]] export type UNCPatternList = [ p0: '', p1: '', p2: string, p3: string, ...rest: MMPattern[], ] export type DrivePatternList = [p0: string, ...rest: MMPattern[]] export type AbsolutePatternList = [p0: '', ...rest: MMPattern[]] export type GlobList = [p: string, ...rest: string[]] const isPatternList = (pl: MMPattern[]): pl is PatternList => pl.length >= 1 const isGlobList = (gl: string[]): gl is GlobList => gl.length >= 1 const customInspect = Symbol.for('nodejs.util.inspect.custom') /** * An immutable-ish view on an array of glob parts and their parsed * results */ export class Pattern { readonly #patternList: PatternList readonly #globList: GlobList readonly #index: number readonly length: number readonly #platform: NodeJS.Platform #rest?: Pattern | null #globString?: string #isDrive?: boolean #isUNC?: boolean #isAbsolute?: boolean #followGlobstar: boolean = true constructor( patternList: MMPattern[], globList: string[], index: number, platform: NodeJS.Platform, ) { if (!isPatternList(patternList)) { throw new TypeError('empty pattern list') } if (!isGlobList(globList)) { throw new TypeError('empty glob list') } if (globList.length !== patternList.length) { throw new TypeError('mismatched pattern list and glob list lengths') } this.length = patternList.length if (index < 0 || index >= this.length) { throw new TypeError('index out of range') } this.#patternList = patternList this.#globList = globList this.#index = index this.#platform = platform // normalize root entries of absolute patterns on initial creation. if (this.#index === 0) { // c: => ['c:/'] // C:/ => ['C:/'] // C:/x => ['C:/', 'x'] // //host/share => ['//host/share/'] // //host/share/ => ['//host/share/'] // //host/share/x => ['//host/share/', 'x'] // //?/UNC/host/share/x => ['//?/UNC/host/share/', 'x'] // /etc => ['/', 'etc'] // / => ['/'] if (this.isUNC()) { // '' / '' / 'host' / 'share', or // '' / '' / '?' / 'UNC' / 'host' / 'share' const n = this.#isLongUNC() ? 6 : 4 const proot = this.#patternList.slice(0, n) const groot = this.#globList.slice(0, n) const prest = this.#patternList.slice(n) const grest = this.#globList.slice(n) if (prest[0] === '') { // ends in / prest.shift() grest.shift() } const p = [...proot, ''].join('/') const g = [...groot, ''].join('/') this.#patternList = [p, ...prest] this.#globList = [g, ...grest] this.length = this.#patternList.length } else if (this.isDrive() || this.isAbsolute()) { const [p1, ...prest] = this.#patternList const [g1, ...grest] = this.#globList if (prest[0] === '') { // ends in / prest.shift() grest.shift() } const p = `${p1 as string}/` const g = `${g1}/` this.#patternList = [p, ...prest] this.#globList = [g, ...grest] this.length = this.#patternList.length } } } [customInspect]() { return `Pattern <${this.#globList.slice(this.#index).join('/')}>` } /** * The first entry in the parsed list of patterns */ pattern(): MMPattern { return this.#patternList[this.#index] as MMPattern } /** * true of if pattern() returns a string */ isString(): boolean { return typeof this.#patternList[this.#index] === 'string' } /** * true of if pattern() returns GLOBSTAR */ isGlobstar(): boolean { return this.#patternList[this.#index] === GLOBSTAR } /** * true if pattern() returns a Matcher, for a portion with magic */ isMatcher(): boolean { return typeof this.#patternList[this.#index] === 'object' } /** * The /-joined set of glob parts that make up this pattern */ globString(): string { return (this.#globString = this.#globString || (this.#index === 0 ? this.isAbsolute() ? this.#globList[0] + this.#globList.slice(1).join('/') : this.#globList.join('/') : this.#globList.slice(this.#index).join('/'))) } /** * true if there are more pattern parts after this one */ hasMore(): boolean { return this.length > this.#index + 1 } /** * The rest of the pattern after this part, or null if this is the end */ rest(): Pattern | null { if (this.#rest !== undefined) return this.#rest if (!this.hasMore()) return (this.#rest = null) this.#rest = new Pattern( this.#patternList, this.#globList, this.#index + 1, this.#platform, ) this.#rest.#isAbsolute = this.#isAbsolute this.#rest.#isUNC = this.#isUNC this.#rest.#isDrive = this.#isDrive return this.#rest } // the root is //?/UNC/host/share, the same as //host/share #isLongUNC(): boolean { const pl = this.#patternList return ( pl[2] === '?' && typeof pl[3] === 'string' && /^unc$/i.test(pl[3]) ) } /** * true if the pattern represents a //unc/path/ on windows */ isUNC(): boolean { const pl = this.#patternList // the host and share, after //?/UNC/ in a long UNC path const h = this.#isLongUNC() ? 4 : 2 return this.#isUNC !== undefined ? this.#isUNC : (this.#isUNC = this.#platform === 'win32' && this.#index === 0 && pl[0] === '' && pl[1] === '' && typeof pl[h] === 'string' && !!pl[h] && typeof pl[h + 1] === 'string' && !!pl[h + 1]) } // pattern like C:/... // split = ['C:', ...] // XXX: would be nice to handle patterns like `c:*` to test the cwd // in c: for *, but I don't know of a way to even figure out what that // cwd is without actually chdir'ing into it? /** * True if the pattern starts with a drive letter on Windows */ isDrive(): boolean { const pl = this.#patternList return this.#isDrive !== undefined ? this.#isDrive : (this.#isDrive = this.#platform === 'win32' && this.#index === 0 && this.length > 1 && typeof pl[0] === 'string' && /^[a-z]:$/i.test(pl[0])) } // pattern = '/' or '/...' or '/x/...' // split = ['', ''] or ['', ...] or ['', 'x', ...] // Drive and UNC both considered absolute on windows /** * True if the pattern is rooted on an absolute path */ isAbsolute(): boolean { const pl = this.#patternList return this.#isAbsolute !== undefined ? this.#isAbsolute : (this.#isAbsolute = (pl[0] === '' && pl.length > 1) || this.isDrive() || this.isUNC()) } /** * consume the root of the pattern, and return it */ root(): string { const p = this.#patternList[0] return ( typeof p === 'string' && this.isAbsolute() && this.#index === 0 ) ? p : '' } /** * Check to see if the current globstar pattern is allowed to follow * a symbolic link. */ checkFollowGlobstar(): boolean { return !( this.#index === 0 || !this.isGlobstar() || !this.#followGlobstar ) } /** * Mark that the current globstar pattern is following a symbolic link */ markFollowGlobstar(): boolean { if (this.#index === 0 || !this.isGlobstar() || !this.#followGlobstar) return false this.#followGlobstar = false return true } }