@@ -11,9 +11,11 @@ namespace SimpleZipDrive.Core;
1111public static class ZipFsHelpers
1212{
1313 // Cache for compiled regex patterns to avoid recompilation overhead.
14- // Uses a plain Dictionary guarded by a lock because all accesses are protected
15- // by the same lock to ensure atomic check-evict-add semantics.
16- private static readonly Dictionary < string , Regex > RegexCache = new ( ) ;
14+ // Uses a Dictionary + LinkedList for LRU eviction: the LinkedList tracks
15+ // insertion/re-access order (most recent at the end), and when the cache
16+ // is full the oldest entry (first in LinkedList) is evicted.
17+ private static readonly Dictionary < string , ( Regex Regex , LinkedListNode < string > Node ) > RegexCache = new ( ) ;
18+ private static readonly LinkedList < string > RegexLruOrder = new ( ) ;
1719 private static readonly object RegexCacheLock = new ( ) ;
1820 private const int MaxRegexCacheSize = 100 ; // Limit cache size to prevent unbounded growth
1921
@@ -233,20 +235,29 @@ internal static bool IsMatchSimple(string input, string pattern)
233235
234236 lock ( RegexCacheLock )
235237 {
236- if ( RegexCache . TryGetValue ( regexPattern , out var regex ) )
237- return regex . IsMatch ( input ) ;
238+ if ( RegexCache . TryGetValue ( regexPattern , out var entry ) )
239+ {
240+ // Move to end of LRU list (most recently used)
241+ RegexLruOrder . Remove ( entry . Node ) ;
242+ var newNode = RegexLruOrder . AddLast ( regexPattern ) ;
243+ RegexCache [ regexPattern ] = ( entry . Regex , newNode ) ;
244+ return entry . Regex . IsMatch ( input ) ;
245+ }
238246
239247 if ( RegexCache . Count >= MaxRegexCacheSize )
240248 {
241- var oldestKey = RegexCache . Keys . FirstOrDefault ( ) ;
242- if ( oldestKey != null )
249+ // Evict the least recently used (first in the linked list)
250+ var oldest = RegexLruOrder . First ;
251+ if ( oldest != null )
243252 {
244- RegexCache . Remove ( oldestKey ) ;
253+ RegexLruOrder . RemoveFirst ( ) ;
254+ RegexCache . Remove ( oldest . Value ) ;
245255 }
246256 }
247257
248258 var newRegex = new Regex ( regexPattern , RegexOptions . IgnoreCase | RegexOptions . Compiled , TimeSpan . FromMilliseconds ( 100 ) ) ;
249- RegexCache [ regexPattern ] = newRegex ;
259+ var lruNode = RegexLruOrder . AddLast ( regexPattern ) ;
260+ RegexCache [ regexPattern ] = ( newRegex , lruNode ) ;
250261 return newRegex . IsMatch ( input ) ;
251262 }
252263 }
@@ -270,4 +281,52 @@ internal static bool IsNameMatch(string name, string pattern)
270281
271282 return IsMatchSimple ( name , pattern ) ;
272283 }
284+
285+ private const int MaxVolumeLabelLength = 32 ;
286+ private static readonly char [ ] InvalidVolumeLabelChars = [ '\\ ' , '/' , ':' , '*' , '?' , '"' , '<' , '>' , '|' ] ;
287+
288+ /// <summary>
289+ /// Sanitizes a string for use as a Windows volume label.
290+ /// Strips invalid characters, trims whitespace, truncates to 32 characters,
291+ /// and falls back to <see cref="ZipFileSystemCore.DefaultVolumeLabel"/> if the result is empty.
292+ /// </summary>
293+ internal static string SanitizeVolumeLabel ( string ? label )
294+ {
295+ if ( string . IsNullOrWhiteSpace ( label ) )
296+ return ZipFileSystemCore . DefaultVolumeLabel ;
297+
298+ Span < char > buffer = stackalloc char [ label . Length ] ;
299+ var written = 0 ;
300+
301+ foreach ( var ch in label )
302+ {
303+ if ( Array . IndexOf ( InvalidVolumeLabelChars , ch ) < 0 )
304+ {
305+ buffer [ written ++ ] = ch ;
306+ }
307+ }
308+
309+ // Trim trailing spaces/dots (Windows trims these)
310+ while ( written > 0 && ( buffer [ written - 1 ] == ' ' || buffer [ written - 1 ] == '.' ) )
311+ {
312+ written -- ;
313+ }
314+
315+ // Truncate to NTFS limit
316+ if ( written > MaxVolumeLabelLength )
317+ {
318+ written = MaxVolumeLabelLength ;
319+ }
320+
321+ // Final trim in case truncation exposed trailing spaces/dots
322+ while ( written > 0 && ( buffer [ written - 1 ] == ' ' || buffer [ written - 1 ] == '.' ) )
323+ {
324+ written -- ;
325+ }
326+
327+ if ( written == 0 )
328+ return ZipFileSystemCore . DefaultVolumeLabel ;
329+
330+ return new string ( buffer [ ..written ] ) ;
331+ }
273332}
0 commit comments