1-
21package org .example ;
32
43import java .io .IOException ;
76import java .util .concurrent .atomic .AtomicLong ;
87import java .util .logging .Logger ;
98import java .util .logging .Level ;
9+ import java .net .URLDecoder ;
10+ import java .nio .charset .StandardCharsets ;
1011
1112/**
1213 * Thread-safe in-memory cache filter using ConcurrentHashMap
@@ -55,7 +56,7 @@ public byte[] getOrFetch(String uri, FileProvider provider) throws IOException {
5556 CacheEntry entry = cache .get (uri );
5657 if (entry != null ) {
5758 entry .recordAccess ();
58- LOGGER .log (Level .FINE , "✓ Cache hit for: " + uri );
59+ LOGGER .log (Level .FINE , " Cache hit for: " + uri );
5960 return entry .data ;
6061 }
6162
@@ -65,14 +66,14 @@ public byte[] getOrFetch(String uri, FileProvider provider) throws IOException {
6566 entry = cache .get (uri );
6667 if (entry != null ) {
6768 entry .recordAccess ();
68- LOGGER .log (Level .FINE , "✓ Cache hit for: " + uri + " (from concurrent fetch)" );
69+ LOGGER .log (Level .FINE , "Cache hit for: " + uri + " (from concurrent fetch)" );
6970 return entry .data ;
7071 }
7172
7273
7374
7475 // Fetch och cachelagra
75- LOGGER .log (Level .FINE , "✗ Cache miss for: " + uri );
76+ LOGGER .log (Level .FINE , "Cache miss for: " + uri );
7677 byte [] fileBytes = provider .fetch (uri );
7778
7879 if (fileBytes != null ) {
@@ -86,7 +87,7 @@ public byte[] getOrFetch(String uri, FileProvider provider) throws IOException {
8687 private void addToCacheUnsafe (String uri , byte [] data ) {
8788 // Guard mot oversized entries som kan blockera eviction
8889 if (data .length > MAX_CACHE_BYTES ) {
89- LOGGER .log (Level .WARNING , "⚠️ Skipping cache for oversized file: " + uri +
90+ LOGGER .log (Level .WARNING , "Skipping cache for oversized file: " + uri +
9091 " (" + (data .length / 1024 / 1024 ) + "MB > " +
9192 (MAX_CACHE_BYTES / 1024 / 1024 ) + "MB)" );
9293 return ;
@@ -99,7 +100,7 @@ private void addToCacheUnsafe(String uri, byte[] data) {
99100
100101 // Om cache fortfarande är full efter eviction, hoppa över
101102 if (shouldEvict (data )) {
102- LOGGER .log (Level .WARNING , "⚠️ Cache full, skipping: " + uri );
103+ LOGGER .log (Level .WARNING , " Cache full, skipping: " + uri );
103104 return ;
104105 }
105106
@@ -137,7 +138,7 @@ private void evictLeastRecentlyUsedUnsafe() {
137138 CacheEntry removed = cache .remove (keyToRemove );
138139 if (removed != null ) {
139140 currentBytes .addAndGet (-removed .data .length );
140- LOGGER .log (Level .FINE , "✗ Evicted from cache: " + keyToRemove +
141+ LOGGER .log (Level .FINE , " Evicted from cache: " + keyToRemove +
141142 " (accesses: " + removed .accessCount .get () + ")" );
142143 }
143144 }
@@ -153,19 +154,79 @@ public void clearCache() {
153154 currentBytes .set (0 );
154155 }
155156 }
156-
157+ /**
158+ * Cache statistics record.
159+ */
157160 @ Override
158- public CacheStats getStats () {
159- long totalAccesses = cache .values ().stream ()
160- .mapToLong (e -> e .accessCount .get ())
161- .sum ();
162-
163- return new CacheStats (
164- cache .size (),
165- currentBytes .get (),
166- MAX_CACHE_ENTRIES ,
167- MAX_CACHE_BYTES ,
168- totalAccesses
161+ public FileCache .CacheStats getStats () {
162+ return null ;
163+ }
164+
165+
166+ class CacheStats {
167+ public final int entries ;
168+ public final long bytes ;
169+ public final int maxEntries ;
170+ public final long maxBytes ;
171+ public final long totalAccesses ;
172+
173+ public CacheStats (int entries , long bytes , int maxEntries , long maxBytes , long totalAccesses ) {
174+ this .entries = entries ;
175+ this .bytes = bytes ;
176+ this .maxEntries = maxEntries ;
177+ this .maxBytes = maxBytes ;
178+ this .totalAccesses = totalAccesses ;
179+ }
180+
181+ @ Override
182+ public String toString () {
183+ String bytesFormatted = formatBytes (bytes );
184+ String maxBytesFormatted = formatBytes (maxBytes );
185+
186+ return String .format (
187+ "CacheStats{entries=%d/%d, bytes=%s/%s, utilization=%.1f%%, accesses=%d}" ,
188+ entries , maxEntries , bytesFormatted , maxBytesFormatted ,
189+ (double ) bytes / maxBytes * 100 , totalAccesses
190+ );
191+ }
192+
193+ private static String formatBytes (long bytes ) {
194+ if (bytes <= 0 ) return "0 B" ;
195+ final String [] units = new String []{"B" , "KB" , "MB" , "GB" };
196+ int digitGroups = (int ) (Math .log10 (bytes ) / Math .log10 (1024 ));
197+ return String .format ("%.1f %s" , bytes / Math .pow (1024 , digitGroups ), units [digitGroups ]);
198+ }
199+ }
200+
201+ /**
202+ * Sanitizes URI by removing query strings, fragments, null bytes, and leading slashes.
203+ * Also performs URL-decoding to normalize percent-encoded sequences.
204+ */
205+ private String sanitizeUri (String uri ) {
206+ if (uri == null || uri .isEmpty ()) {
207+ return "index.html" ;
208+ }
209+
210+ // Ta bort query string och fragment
211+ int queryIndex = uri .indexOf ('?' );
212+ int fragmentIndex = uri .indexOf ('#' );
213+ int endIndex = Math .min (
214+ queryIndex > 0 ? queryIndex : uri .length (),
215+ fragmentIndex > 0 ? fragmentIndex : uri .length ()
169216 );
217+
218+ uri = uri .substring (0 , endIndex )
219+ .replace ("\0 " , "" )
220+ .replaceAll ("^/+" , "" ); // Bort med leading slashes
221+
222+ // URL-decode för att normalisera percent-encoded sequences (t.ex. %2e%2e -> ..)
223+ try {
224+ uri = URLDecoder .decode (uri , StandardCharsets .UTF_8 );
225+ } catch (IllegalArgumentException e ) {
226+ LOGGER .log (Level .WARNING , "Ogiltig URL-kodning i URI: " + uri );
227+ // Returna som den är om avkodning misslyckas; isPathTraversal kommer hantera det
228+ }
229+
230+ return uri .isEmpty () ? "index.html" : uri ;
170231 }
171232}
0 commit comments