11use anyhow:: { anyhow, Result } ;
22use base64:: Engine ;
3- use chrono:: Utc ;
3+ use chrono:: { DateTime , Utc } ;
44use reqwest:: header:: { HeaderMap , HeaderValue } ;
55use reqwest:: multipart:: { Form , Part } ;
66use reqwest:: Client ;
@@ -650,7 +650,7 @@ pub async fn submit_solution<P: AsRef<Path>>(
650650 {
651651 for ( key, run_data) in runs. iter ( ) {
652652 if key. starts_with ( "profile" ) {
653- handle_profile_result ( cb, run_data, i) ;
653+ handle_profile_result ( cb, run_data, i, key ) ;
654654 }
655655 }
656656 }
@@ -754,7 +754,12 @@ pub async fn submit_solution<P: AsRef<Path>>(
754754
755755/// Handle profile mode results by decoding and displaying profile data,
756756/// and saving trace files to the current directory.
757- fn handle_profile_result ( cb : & ( dyn Fn ( String ) + Send + Sync ) , run_data : & Value , run_idx : usize ) {
757+ fn handle_profile_result (
758+ cb : & ( dyn Fn ( String ) + Send + Sync ) ,
759+ run_data : & Value ,
760+ result_idx : usize ,
761+ run_key : & str ,
762+ ) {
758763 // 1. Get profiler type and display it
759764 if let Some ( profile) = run_data. get ( "profile" ) {
760765 let profiler = profile
@@ -814,11 +819,9 @@ fn handle_profile_result(cb: &(dyn Fn(String) + Send + Sync), run_data: &Value,
814819 if !trace_b64. is_empty ( ) {
815820 match base64:: engine:: general_purpose:: STANDARD . decode ( trace_b64) {
816821 Ok ( trace_data) => {
817- // Generate unique filename with timestamp and run index
818- let timestamp = Utc :: now ( ) . format ( "%Y%m%d_%H%M%S" ) ;
819- let filename = format ! ( "profile_{}_run{}.zip" , timestamp, run_idx) ;
820- match std:: fs:: write ( & filename, & trace_data) {
821- Ok ( _) => cb ( format ! ( "\n Saved profile trace to: {}" , filename) ) ,
822+ match write_profile_trace_file ( & trace_data, Utc :: now ( ) , result_idx, run_key)
823+ {
824+ Ok ( filename) => cb ( format ! ( "\n Saved profile trace to: {}" , filename) ) ,
822825 Err ( e) => cb ( format ! ( "Failed to save trace file: {}" , e) ) ,
823826 }
824827 }
@@ -836,9 +839,55 @@ fn handle_profile_result(cb: &(dyn Fn(String) + Send + Sync), run_data: &Value,
836839 }
837840}
838841
842+ fn sanitize_profile_run_key ( run_key : & str ) -> String {
843+ let sanitized: String = run_key
844+ . chars ( )
845+ . map ( |ch| {
846+ if ch. is_ascii_alphanumeric ( ) || ch == '_' || ch == '-' {
847+ ch
848+ } else {
849+ '_'
850+ }
851+ } )
852+ . collect ( ) ;
853+
854+ if sanitized. is_empty ( ) {
855+ "profile" . to_string ( )
856+ } else {
857+ sanitized
858+ }
859+ }
860+
861+ fn build_profile_trace_filename (
862+ timestamp : DateTime < Utc > ,
863+ result_idx : usize ,
864+ run_key : & str ,
865+ ) -> String {
866+ let run_key = sanitize_profile_run_key ( run_key) ;
867+ format ! (
868+ "profile_{}_result{}_{}.zip" ,
869+ timestamp. format( "%Y%m%d_%H%M%S" ) ,
870+ result_idx,
871+ run_key
872+ )
873+ }
874+
875+ fn write_profile_trace_file (
876+ trace_data : & [ u8 ] ,
877+ timestamp : DateTime < Utc > ,
878+ result_idx : usize ,
879+ run_key : & str ,
880+ ) -> std:: io:: Result < String > {
881+ let filename = build_profile_trace_filename ( timestamp, result_idx, run_key) ;
882+ std:: fs:: write ( & filename, trace_data) ?;
883+ Ok ( filename)
884+ }
885+
839886#[ cfg( test) ]
840887mod tests {
841888 use super :: * ;
889+ use chrono:: TimeZone ;
890+ use tempfile:: tempdir;
842891
843892 #[ test]
844893 fn test_create_client_without_cli_id ( ) {
@@ -932,4 +981,64 @@ mod tests {
932981 std:: env:: set_var ( "POPCORN_API_URL" , val) ;
933982 }
934983 }
984+
985+ #[ test]
986+ fn test_build_profile_trace_filename_uses_result_index_and_run_key ( ) {
987+ let timestamp = Utc
988+ . with_ymd_and_hms ( 2026 , 3 , 27 , 9 , 38 , 46 )
989+ . single ( )
990+ . unwrap ( ) ;
991+
992+ let filename = build_profile_trace_filename ( timestamp, 0 , "profile3" ) ;
993+
994+ assert_eq ! ( filename, "profile_20260327_093846_result0_profile3.zip" ) ;
995+ }
996+
997+ #[ test]
998+ fn test_build_profile_trace_filename_sanitizes_run_key ( ) {
999+ let timestamp = Utc
1000+ . with_ymd_and_hms ( 2026 , 3 , 27 , 9 , 38 , 46 )
1001+ . single ( )
1002+ . unwrap ( ) ;
1003+
1004+ let filename = build_profile_trace_filename ( timestamp, 1 , "profile:1/a b" ) ;
1005+
1006+ assert_eq ! (
1007+ filename,
1008+ "profile_20260327_093846_result1_profile_1_a_b.zip"
1009+ ) ;
1010+ }
1011+
1012+ #[ test]
1013+ fn test_build_profile_trace_filename_uses_default_run_key_when_empty ( ) {
1014+ let timestamp = Utc
1015+ . with_ymd_and_hms ( 2026 , 3 , 27 , 9 , 38 , 46 )
1016+ . single ( )
1017+ . unwrap ( ) ;
1018+
1019+ let filename = build_profile_trace_filename ( timestamp, 2 , "" ) ;
1020+
1021+ assert_eq ! ( filename, "profile_20260327_093846_result2_profile.zip" ) ;
1022+ }
1023+
1024+ #[ test]
1025+ fn test_write_profile_trace_file_writes_expected_contents ( ) {
1026+ let temp_dir = tempdir ( ) . unwrap ( ) ;
1027+ let original_dir = std:: env:: current_dir ( ) . unwrap ( ) ;
1028+ let timestamp = Utc
1029+ . with_ymd_and_hms ( 2026 , 3 , 27 , 9 , 38 , 46 )
1030+ . single ( )
1031+ . unwrap ( ) ;
1032+ let trace_data = b"trace-bytes" ;
1033+
1034+ std:: env:: set_current_dir ( temp_dir. path ( ) ) . unwrap ( ) ;
1035+
1036+ let filename = write_profile_trace_file ( trace_data, timestamp, 3 , "profile/3" ) . unwrap ( ) ;
1037+ let written_path = temp_dir. path ( ) . join ( & filename) ;
1038+
1039+ assert_eq ! ( filename, "profile_20260327_093846_result3_profile_3.zip" ) ;
1040+ assert_eq ! ( std:: fs:: read( & written_path) . unwrap( ) , trace_data) ;
1041+
1042+ std:: env:: set_current_dir ( original_dir) . unwrap ( ) ;
1043+ }
9351044}
0 commit comments