66#include "sentry_session.h"
77#include "sentry_uuid.h"
88#include <errno.h>
9+ #include <stdlib.h>
910#include <string.h>
1011
1112sentry_run_t *
@@ -237,6 +238,13 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
237238 if (strcmp (options -> run -> run_path -> path , run_dir -> path ) == 0 ) {
238239 continue ;
239240 }
241+
242+ sentry_path_t * cache_dir = NULL ;
243+ if (options -> cache_keep ) {
244+ cache_dir = sentry__path_join_str (options -> database_path , "cache" );
245+ sentry__path_create_dir_all (cache_dir );
246+ }
247+
240248 sentry_pathiter_t * run_iter = sentry__path_iter_directory (run_dir );
241249 const sentry_path_t * file ;
242250 while (run_iter && (file = sentry__pathiter_next (run_iter )) != NULL ) {
@@ -281,12 +289,24 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
281289 } else if (sentry__path_ends_with (file , ".envelope" )) {
282290 sentry_envelope_t * envelope = sentry__envelope_from_path (file );
283291 sentry__capture_envelope (options -> transport , envelope );
292+
293+ if (options -> cache_keep ) {
294+ sentry_path_t * cached_file = sentry__path_join_str (
295+ cache_dir , sentry__path_filename (file ));
296+ sentry__path_rename (file , cached_file );
297+ sentry__path_free (cached_file );
298+ continue ;
299+ }
284300 }
285301
286302 sentry__path_remove (file );
287303 }
288304 sentry__pathiter_free (run_iter );
289305
306+ if (options -> cache_keep ) {
307+ sentry__path_free (cache_dir );
308+ }
309+
290310 sentry__path_remove_all (run_dir );
291311 sentry__filelock_free (lock );
292312 }
@@ -295,6 +315,123 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
295315 sentry__capture_envelope (options -> transport , session_envelope );
296316}
297317
318+ // Cache Pruning below is based on prune_crash_reports.cc from Crashpad
319+
320+ /**
321+ * A cache entry with its metadata for sorting and pruning decisions.
322+ */
323+ typedef struct {
324+ sentry_path_t * path ;
325+ time_t mtime ;
326+ size_t size ;
327+ } cache_entry_t ;
328+
329+ /**
330+ * Comparison function to sort cache entries by mtime, newest first.
331+ */
332+ static int
333+ compare_cache_entries_newest_first (const void * a , const void * b )
334+ {
335+ const cache_entry_t * entry_a = (const cache_entry_t * )a ;
336+ const cache_entry_t * entry_b = (const cache_entry_t * )b ;
337+ // Newest first: if b is newer, return positive (b comes before a)
338+ if (entry_b -> mtime > entry_a -> mtime ) {
339+ return 1 ;
340+ }
341+ if (entry_b -> mtime < entry_a -> mtime ) {
342+ return -1 ;
343+ }
344+ return 0 ;
345+ }
346+
347+ void
348+ sentry__cleanup_cache (const sentry_options_t * options )
349+ {
350+ if (!options -> database_path ) {
351+ return ;
352+ }
353+
354+ sentry_path_t * cache_dir
355+ = sentry__path_join_str (options -> database_path , "cache" );
356+ if (!sentry__path_is_dir (cache_dir )) {
357+ sentry__path_free (cache_dir );
358+ return ;
359+ }
360+
361+ // First pass: collect all cache entries with their metadata
362+ size_t entries_capacity = 16 ;
363+ size_t entries_count = 0 ;
364+ cache_entry_t * entries
365+ = sentry_malloc (sizeof (cache_entry_t ) * entries_capacity );
366+ if (!entries ) {
367+ sentry__path_free (cache_dir );
368+ return ;
369+ }
370+
371+ sentry_pathiter_t * iter = sentry__path_iter_directory (cache_dir );
372+ const sentry_path_t * entry ;
373+ while (iter && (entry = sentry__pathiter_next (iter )) != NULL ) {
374+ if (sentry__path_is_dir (entry )) {
375+ continue ;
376+ }
377+
378+ // Grow array if needed
379+ if (entries_count >= entries_capacity ) {
380+ entries_capacity *= 2 ;
381+ cache_entry_t * new_entries
382+ = sentry_malloc (sizeof (cache_entry_t ) * entries_capacity );
383+ if (!new_entries ) {
384+ break ;
385+ }
386+ memcpy (new_entries , entries , sizeof (cache_entry_t ) * entries_count );
387+ sentry_free (entries );
388+ entries = new_entries ;
389+ }
390+
391+ entries [entries_count ].path = sentry__path_clone (entry );
392+ entries [entries_count ].mtime = sentry__path_get_mtime (entry );
393+ entries [entries_count ].size = sentry__path_get_size (entry );
394+ entries_count ++ ;
395+ }
396+ sentry__pathiter_free (iter );
397+
398+ // Sort by mtime, newest first (like crashpad)
399+ // This ensures we keep the newest entries when pruning by size
400+ qsort (entries , entries_count , sizeof (cache_entry_t ),
401+ compare_cache_entries_newest_first );
402+
403+ // Calculate the age threshold
404+ time_t now = time (NULL );
405+ time_t oldest_allowed = now - options -> cache_max_age ;
406+
407+ // Prune entries: iterate newest-to-oldest, accumulating size
408+ // Remove if: too old OR accumulated size exceeds limit
409+ size_t accumulated_size = 0 ;
410+ for (size_t i = 0 ; i < entries_count ; i ++ ) {
411+ bool should_prune = false;
412+
413+ // Age-based pruning
414+ if (options -> cache_max_age > 0 && entries [i ].mtime < oldest_allowed ) {
415+ should_prune = true;
416+ }
417+
418+ // Size-based pruning (accumulate size as we go, like crashpad)
419+ accumulated_size += entries [i ].size ;
420+ if (options -> cache_max_size > 0
421+ && accumulated_size > options -> cache_max_size ) {
422+ should_prune = true;
423+ }
424+
425+ if (should_prune ) {
426+ sentry__path_remove_all (entries [i ].path );
427+ }
428+ sentry__path_free (entries [i ].path );
429+ }
430+
431+ sentry_free (entries );
432+ sentry__path_free (cache_dir );
433+ }
434+
298435static const char * g_last_crash_filename = "last_crash" ;
299436
300437bool
0 commit comments