11// Copyright (c) Six Labors.
22// Licensed under the Apache License, Version 2.0.
33
4+ using System ;
45using System . IO ;
6+ using System . Runtime . CompilerServices ;
7+ using System . Runtime . InteropServices ;
58using System . Threading . Tasks ;
69using Microsoft . AspNetCore . Hosting ;
710using Microsoft . Extensions . FileProviders ;
@@ -21,6 +24,11 @@ public class PhysicalFileSystemCache : IImageCache
2124 /// </summary>
2225 private readonly string cacheRootPath ;
2326
27+ /// <summary>
28+ /// The length of the filename to use (minus the extension) when storing images in the image cache.
29+ /// </summary>
30+ private readonly int cachedNameLength ;
31+
2432 /// <summary>
2533 /// The file provider abstraction.
2634 /// </summary>
@@ -69,6 +77,7 @@ public PhysicalFileSystemCache(
6977 Directory . CreateDirectory ( this . cacheRootPath ) ;
7078 }
7179
80+ this . cachedNameLength = ( int ) this . options . CachedNameLength ;
7281 this . fileProvider = new PhysicalFileProvider ( this . cacheRootPath ) ;
7382 this . options = options . Value ;
7483 this . formatUtilies = formatUtilities ;
@@ -77,7 +86,7 @@ public PhysicalFileSystemCache(
7786 /// <inheritdoc/>
7887 public async Task < IImageCacheResolver > GetAsync ( string key )
7988 {
80- string path = this . ToFilePath ( key ) ;
89+ string path = ToFilePath ( key , this . cachedNameLength ) ;
8190
8291 IFileInfo metaFileInfo = this . fileProvider . GetFileInfo ( this . ToMetaDataFilePath ( path ) ) ;
8392 if ( ! metaFileInfo . Exists )
@@ -105,7 +114,7 @@ public async Task<IImageCacheResolver> GetAsync(string key)
105114 /// <inheritdoc/>
106115 public async Task SetAsync ( string key , Stream stream , ImageCacheMetadata metadata )
107116 {
108- string path = Path . Combine ( this . cacheRootPath , this . ToFilePath ( key ) ) ;
117+ string path = Path . Combine ( this . cacheRootPath , ToFilePath ( key , this . cachedNameLength ) ) ;
109118 string imagePath = this . ToImageFilePath ( path , metadata ) ;
110119 string metaPath = this . ToMetaDataFilePath ( path ) ;
111120 string directory = Path . GetDirectoryName ( path ) ;
@@ -146,8 +155,36 @@ private string ToImageFilePath(string path, in ImageCacheMetadata metaData)
146155 /// Converts the key into a nested file path.
147156 /// </summary>
148157 /// <param name="key">The cache key.</param>
158+ /// <param name="cachedNameLength">The length of the cached file name minus the extension.</param>
149159 /// <returns>The <see cref="string"/>.</returns>
150- private string ToFilePath ( string key ) // TODO: Avoid the allocation here.
151- => $ "{ string . Join ( "/" , key . Substring ( 0 , ( int ) this . options . CachedNameLength ) . ToCharArray ( ) ) } /{ key } ";
160+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
161+ internal static unsafe string ToFilePath ( string key , int cachedNameLength )
162+ {
163+ const char separator = '/' ;
164+
165+ // Each key substring char + separator + key
166+ int length = ( cachedNameLength * 2 ) + key . Length ;
167+ fixed ( char * keyPtr = key )
168+ {
169+ return string . Create ( length , ( Ptr : ( IntPtr ) keyPtr , key . Length ) , ( chars , args ) =>
170+ {
171+ var keySpan = new ReadOnlySpan < char > ( ( char * ) args . Ptr , args . Length ) ;
172+ ref char keyRef = ref MemoryMarshal . GetReference ( keySpan ) ;
173+ ref char charRef = ref MemoryMarshal . GetReference ( chars ) ;
174+
175+ int index = 0 ;
176+ for ( int i = 0 ; i < cachedNameLength ; i ++ )
177+ {
178+ Unsafe . Add ( ref charRef , index ++ ) = Unsafe. Add ( ref keyRef , i ) ;
179+ Unsafe . Add ( ref charRef , index ++ ) = separator;
180+ }
181+
182+ for ( int i = 0 ; i < keySpan . Length ; i ++ )
183+ {
184+ Unsafe . Add ( ref charRef , index ++ ) = Unsafe. Add ( ref keyRef , i ) ;
185+ }
186+ } ) ;
187+ }
188+ }
152189 }
153190}
0 commit comments