@@ -99,28 +99,64 @@ export async function getSshConfiguration(configurationPath: string, resolveIncl
9999 return config ;
100100}
101101
102- async function resolveConfigIncludes ( config : Configuration , configPath : string ) : Promise < void > {
103- for ( const entry of config ) {
104- if ( isDirective ( entry ) && entry . param === 'Include' ) {
105- let includePath : string = resolveHome ( entry . value ) ;
106- if ( isWindows && ! ! includePath . match ( / ^ \/ [ a - z ] : / i) ) {
107- includePath = includePath . substr ( 1 ) ;
108- }
109-
110- if ( ! path . isAbsolute ( includePath ) ) {
111- includePath = path . resolve ( path . dirname ( configPath ) , includePath ) ;
112- }
113-
114- const pathsToGetFilesFrom : string [ ] = await globAsync ( includePath ) ;
102+ function getProcessedPathKey ( filePath : string ) : string {
103+ const absolutePath : string = path . resolve ( filePath ) ;
104+ const normalizedPath : string = path . normalize ( absolutePath ) ;
105+ return isWindows ? normalizedPath . toLowerCase ( ) : normalizedPath ;
106+ }
115107
116- for ( const filePath of pathsToGetFilesFrom ) {
117- await getIncludedConfigFile ( config , filePath ) ;
108+ async function resolveConfigIncludes (
109+ config : Configuration ,
110+ configPath : string ,
111+ processedIncludePaths ?: Set < string > ,
112+ processedIncludeEntries ?: WeakSet < ConfigurationDirective >
113+ ) : Promise < void > {
114+ processedIncludePaths = processedIncludePaths ?? new Set < string > ( ) ;
115+ processedIncludeEntries = processedIncludeEntries ?? new WeakSet < ConfigurationDirective > ( ) ;
116+ const configKey : string = getProcessedPathKey ( configPath ) ;
117+ if ( processedIncludePaths . has ( configKey ) ) {
118+ return ;
119+ }
120+ processedIncludePaths . add ( configKey ) ;
121+ try {
122+ for ( const entry of config ) {
123+ if ( isDirective ( entry ) && entry . param === 'Include' ) {
124+ // Prevent duplicate expansion of the same Include directive within a single resolution pass.
125+ if ( processedIncludeEntries . has ( entry ) ) {
126+ continue ;
127+ }
128+ processedIncludeEntries . add ( entry ) ;
129+ let includePath : string = resolveHome ( entry . value ) ;
130+ if ( isWindows && ! ! includePath . match ( / ^ \/ [ a - z ] : / i) ) {
131+ includePath = includePath . slice ( 1 ) ;
132+ }
133+
134+ if ( ! path . isAbsolute ( includePath ) ) {
135+ includePath = path . resolve ( path . dirname ( configPath ) , includePath ) ;
136+ }
137+
138+ const pathsToGetFilesFrom : string [ ] = await globAsync ( includePath ) ;
139+
140+ for ( const filePath of pathsToGetFilesFrom ) {
141+ const includeKey : string = getProcessedPathKey ( filePath ) ;
142+ if ( processedIncludePaths . has ( includeKey ) ) {
143+ continue ;
144+ }
145+ await getIncludedConfigFile ( config , filePath , processedIncludePaths , processedIncludeEntries ) ;
146+ }
118147 }
119148 }
149+ } finally {
150+ processedIncludePaths . delete ( configKey ) ;
120151 }
121152}
122153
123- async function getIncludedConfigFile ( config : Configuration , includePath : string ) : Promise < void > {
154+ async function getIncludedConfigFile (
155+ config : Configuration ,
156+ includePath : string ,
157+ processedIncludePaths : Set < string > ,
158+ processedIncludeEntries : WeakSet < ConfigurationDirective >
159+ ) : Promise < void > {
124160 let includedContents : string ;
125161 try {
126162 includedContents = ( await fs . readFile ( includePath ) ) . toString ( ) ;
@@ -136,6 +172,7 @@ async function getIncludedConfigFile(config: Configuration, includePath: string)
136172 getSshChannel ( ) . appendLine ( localize ( "failed.to.parse.SSH.config" , "Failed to parse SSH configuration file {0}: {1}" , includePath , ( err as Error ) . message ) ) ;
137173 return ;
138174 }
175+ await resolveConfigIncludes ( parsedIncludedContents , includePath , processedIncludePaths , processedIncludeEntries ) ;
139176 config . push ( ...parsedIncludedContents ) ;
140177}
141178
0 commit comments