1+ using Ramstack . Globbing ;
2+
13namespace Ramstack . FileProviders ;
24
35/// <summary>
@@ -22,6 +24,7 @@ public sealed class PrefixedFileProvider : IFileProvider, IDisposable
2224 /// to which the prefix will be applied.</param>
2325 public PrefixedFileProvider ( string prefix , IFileProvider provider )
2426 {
27+ ArgumentNullException . ThrowIfNull ( prefix ) ;
2528 ArgumentNullException . ThrowIfNull ( provider ) ;
2629
2730 prefix = FilePath . Normalize ( prefix ) ;
@@ -45,7 +48,9 @@ public PrefixedFileProvider(string prefix, IFileProvider provider)
4548 /// <inheritdoc />
4649 public IFileInfo GetFileInfo ( string subpath )
4750 {
48- var path = ResolvePath ( FilePath . Normalize ( subpath ) , _prefix ) ;
51+ subpath = FilePath . Normalize ( subpath ) ;
52+
53+ var path = ResolvePath ( _prefix , subpath ) ;
4954 if ( path is not null )
5055 return _provider . GetFileInfo ( path ) ;
5156
@@ -62,7 +67,7 @@ public IDirectoryContents GetDirectoryContents(string subpath)
6267 if ( entry . Path == subpath )
6368 return new ArtificialDirectoryContents ( entry . DirectoryName ) ;
6469
65- var path = ResolvePath ( subpath , _prefix ) ;
70+ var path = ResolvePath ( _prefix , subpath ) ;
6671 if ( path is not null )
6772 return _provider . GetDirectoryContents ( path ) ;
6873
@@ -72,27 +77,110 @@ public IDirectoryContents GetDirectoryContents(string subpath)
7277 /// <inheritdoc />
7378 public IChangeToken Watch ( string filter )
7479 {
75- var path = ResolvePath ( FilePath . Normalize ( filter ) , _prefix ) ;
80+ filter = FilePath . Normalize ( filter ) ;
81+
82+ var path = ResolvePath ( _prefix , filter ) ;
7683 if ( path is not null )
7784 return _provider . Watch ( path ) ;
7885
86+ var pattern = ResolveGlobFilter ( _prefix , filter ) ;
87+ if ( pattern is not null )
88+ return _provider . Watch ( pattern ) ;
89+
7990 return NullChangeToken . Singleton ;
8091 }
8192
8293 /// <inheritdoc />
8394 public void Dispose ( ) =>
8495 ( _provider as IDisposable ) ? . Dispose ( ) ;
8596
86- private static string ? ResolvePath ( string path , string prefix )
97+ private static string ? ResolvePath ( string prefix , string path )
8798 {
8899 Debug . Assert ( path == FilePath . Normalize ( path ) ) ;
89100
90101 if ( path == prefix )
91102 return "/" ;
92103
93- if ( path . StartsWith ( prefix , StringComparison . Ordinal ) && path [ prefix . Length ] == '/' )
94- return path [ prefix . Length ..] ;
104+ if ( path . StartsWith ( prefix , StringComparison . Ordinal ) )
105+ if ( ( uint ) prefix . Length < ( uint ) path . Length )
106+ if ( path [ prefix . Length ] == '/' )
107+ return path [ prefix . Length ..] ;
108+
109+ return null ;
110+ }
111+
112+ /// <summary>
113+ /// Attempts to resolve a glob filter relative to a virtual path prefix,
114+ /// removing any prefix segments that match corresponding parts of the filter.
115+ /// </summary>
116+ /// <param name="prefix">The virtual path prefix representing the base of the current provider.</param>
117+ /// <param name="filter">The incoming glob filter that may include glob patterns.</param>
118+ /// <returns>
119+ /// A normalized filter value that can be safely passed to the wrapped file provider
120+ /// or <see langword="null" /> if the filter cannot be applied.
121+ /// </returns>
122+ /// <remarks>
123+ /// The goal is to determine whether a specified glob filter
124+ /// (e.g., "/modules/*/{assets,css,js}/**/*.{css,js}") applies to this provider, which is
125+ /// virtually mounted at a specific prefix path (e.g., "/modules/profile/assets").
126+ /// </remarks>
127+ private static string ? ResolveGlobFilter ( string prefix , string filter )
128+ {
129+ Debug . Assert ( prefix == FilePath . Normalize ( prefix ) ) ;
130+ Debug . Assert ( filter == FilePath . Normalize ( filter ) ) ;
131+
132+ var prefixSegments = new PathTokenizer ( prefix ) . GetEnumerator ( ) ;
133+ var filterSegments = new PathTokenizer ( filter ) . GetEnumerator ( ) ;
134+
135+ var list = new List < string > ( ) ;
136+
137+ while ( prefixSegments . MoveNext ( ) && filterSegments . MoveNext ( ) )
138+ {
139+ var fs = filterSegments . Current ;
140+
141+ // The globstar '**' matches any number of remaining segments, including none
142+ if ( fs is "**" )
143+ {
144+ // Add '**' and all remaining filter segments to the result.
145+ do
146+ {
147+ var segment = filterSegments . Current . ToString ( ) ;
148+ list . Add ( segment ) ;
149+ }
150+ while ( filterSegments . MoveNext ( ) ) ;
151+
152+ return string . Join ( "/" , list ) ;
153+ }
154+
155+ if ( fs is "*" )
156+ {
157+ // '*' matches any prefix segment, continue matching.
158+ continue ;
159+ }
160+
161+ if ( Matcher . IsMatch ( prefixSegments . Current , fs , MatchFlags . Unix ) )
162+ {
163+ // Segment matches the prefix segment, continue matching.
164+ continue ;
165+ }
166+
167+ // Segment doesn't match the prefix at all.
168+ // This means the filter cannot be applied to the underlying provider.
169+ return null ;
170+ }
171+
172+ if ( ! prefixSegments . MoveNext ( ) )
173+ {
174+ // All prefix segments have been matched and consumed successfully.
175+ // Append all remaining filter segments.
176+ while ( filterSegments . MoveNext ( ) )
177+ list . Add ( filterSegments . Current . ToString ( ) ) ;
178+
179+ return string . Join ( "/" , list ) ;
180+ }
95181
182+ // Not all prefix segments were matched.
183+ // This means the filter cannot be applied to the underlying provider.
96184 return null ;
97185 }
98186
0 commit comments