@@ -2806,6 +2806,153 @@ void testMissingSplashScreenImage(@TempDir Path output) throws IOException {
28062806 assertThat (cmd , not (containsString ("-splash:" )));
28072807 }
28082808
2809+ @ Test
2810+ void testSplashScreenPathTraversal (@ TempDir Path output ) throws IOException {
2811+ // Test path traversal attack prevention
2812+ Path jar = createJar (output , Integer .toString (Runtime .version ().feature ()),
2813+ Collections .singletonMap (Project .ATTR_SPLASH_SCREEN_IMAGE , "../../../etc/passwd" ));
2814+
2815+ CommandLine .ParseResult pr = JBang .getCommandLine ().parseArgs ("run" , jar .toString ());
2816+ Run run = (Run ) pr .subcommand ().commandSpec ().userObject ();
2817+
2818+ ProjectBuilder pb = run .createProjectBuilderForRun ();
2819+ Project code = pb .build (jar .toString ());
2820+
2821+ // Should reject the path and not include -splash flag
2822+ String cmd = CmdGenerator .builder (code ).build ().generate ();
2823+ assertThat (cmd , not (containsString ("-splash:" )));
2824+ }
2825+
2826+ @ Test
2827+ void testSplashScreenAbsolutePath (@ TempDir Path output ) throws IOException {
2828+ // Test absolute path rejection
2829+ Path jar = createJar (output , Integer .toString (Runtime .version ().feature ()),
2830+ Collections .singletonMap (Project .ATTR_SPLASH_SCREEN_IMAGE , "/tmp/splash.png" ));
2831+
2832+ CommandLine .ParseResult pr = JBang .getCommandLine ().parseArgs ("run" , jar .toString ());
2833+ Run run = (Run ) pr .subcommand ().commandSpec ().userObject ();
2834+
2835+ ProjectBuilder pb = run .createProjectBuilderForRun ();
2836+ Project code = pb .build (jar .toString ());
2837+
2838+ // Should reject absolute paths
2839+ String cmd = CmdGenerator .builder (code ).build ().generate ();
2840+ assertThat (cmd , not (containsString ("-splash:" )));
2841+ }
2842+
2843+ @ Test
2844+ void testSplashScreenEmptyPath (@ TempDir Path output ) throws IOException {
2845+ // Test empty path handling
2846+ Path jar = createJar (output , Integer .toString (Runtime .version ().feature ()),
2847+ Collections .singletonMap (Project .ATTR_SPLASH_SCREEN_IMAGE , "" ));
2848+
2849+ CommandLine .ParseResult pr = JBang .getCommandLine ().parseArgs ("run" , jar .toString ());
2850+ Run run = (Run ) pr .subcommand ().commandSpec ().userObject ();
2851+
2852+ ProjectBuilder pb = run .createProjectBuilderForRun ();
2853+ Project code = pb .build (jar .toString ());
2854+
2855+ // Should handle empty path gracefully
2856+ String cmd = CmdGenerator .builder (code ).build ().generate ();
2857+ assertThat (cmd , not (containsString ("-splash:" )));
2858+ }
2859+
2860+ @ Test
2861+ void testSplashScreenCaching (@ TempDir Path output ) throws IOException , InterruptedException {
2862+ // Create a simple 1x1 PNG image
2863+ byte [] pngData = new byte [] {
2864+ (byte ) 0x89 , 0x50 , 0x4E , 0x47 , 0x0D , 0x0A , 0x1A , 0x0A , // PNG header
2865+ 0x00 , 0x00 , 0x00 , 0x0D , 0x49 , 0x48 , 0x44 , 0x52 , // IHDR chunk
2866+ 0x00 , 0x00 , 0x00 , 0x01 , 0x00 , 0x00 , 0x00 , 0x01 , // 1x1 dimensions
2867+ 0x08 , 0x02 , 0x00 , 0x00 , 0x00 , (byte ) 0x90 , (byte ) 0x77 , 0x53 , (byte ) 0xDE , 0x00 , 0x00 , 0x00 , 0x0C ,
2868+ 0x49 , 0x44 , 0x41 , 0x54 , // IDAT chunk
2869+ 0x08 , (byte ) 0xD7 , 0x63 , (byte ) 0xF8 , (byte ) 0xCF , (byte ) 0xC0 , 0x00 , 0x00 , 0x03 , 0x01 , 0x01 , 0x00 ,
2870+ 0x18 , (byte ) 0xDD , (byte ) 0x8D , (byte ) 0xB4 , 0x00 , 0x00 , 0x00 , 0x00 , 0x49 , 0x45 , 0x4E , 0x44 ,
2871+ (byte ) 0xAE , 0x42 , 0x60 , (byte ) 0x82
2872+ };
2873+
2874+ Path jar = output .resolve ("cache-test.jar" );
2875+ Manifest manifest = new Manifest ();
2876+ Attributes attrs = manifest .getMainAttributes ();
2877+ attrs .put (Attributes .Name .MANIFEST_VERSION , "1.0" );
2878+ attrs .put (Attributes .Name .MAIN_CLASS , "test.Main" );
2879+ attrs .putValue (Project .ATTR_SPLASH_SCREEN_IMAGE , "splash.png" );
2880+
2881+ try (JarOutputStream jos = new JarOutputStream (Files .newOutputStream (jar ), manifest )) {
2882+ jos .putNextEntry (new ZipEntry ("splash.png" ));
2883+ jos .write (pngData );
2884+ jos .closeEntry ();
2885+ }
2886+
2887+ // First run - extract splash
2888+ CommandLine .ParseResult pr1 = JBang .getCommandLine ().parseArgs ("run" , jar .toString ());
2889+ Run run1 = (Run ) pr1 .subcommand ().commandSpec ().userObject ();
2890+ ProjectBuilder pb1 = run1 .createProjectBuilderForRun ();
2891+ Project code1 = pb1 .build (jar .toString ());
2892+ String cmd1 = CmdGenerator .builder (code1 ).build ().generate ();
2893+
2894+ assertThat (cmd1 , containsString ("-splash:" ));
2895+ String splashArg1 = Arrays .stream (cmd1 .split (" " ))
2896+ .filter (s -> s .startsWith ("-splash:" ))
2897+ .findFirst ()
2898+ .orElse (null );
2899+ assertNotNull (splashArg1 );
2900+ Path extractedPath = Paths .get (splashArg1 .substring ("-splash:" .length ()));
2901+ assertTrue (Files .exists (extractedPath ), "Splash image should be extracted" );
2902+
2903+ // Get the timestamp of the extracted file
2904+ java .nio .file .attribute .FileTime firstTimestamp = Files .getLastModifiedTime (extractedPath );
2905+
2906+ // Wait a bit to ensure different timestamp if re-extracted
2907+ Thread .sleep (1500 );
2908+
2909+ // Second run - should use cached splash (no re-extraction)
2910+ CommandLine .ParseResult pr2 = JBang .getCommandLine ().parseArgs ("run" , jar .toString ());
2911+ Run run2 = (Run ) pr2 .subcommand ().commandSpec ().userObject ();
2912+ ProjectBuilder pb2 = run2 .createProjectBuilderForRun ();
2913+ Project code2 = pb2 .build (jar .toString ());
2914+ String cmd2 = CmdGenerator .builder (code2 ).build ().generate ();
2915+
2916+ java .nio .file .attribute .FileTime secondTimestamp = Files .getLastModifiedTime (extractedPath );
2917+ assertEquals (firstTimestamp , secondTimestamp ,
2918+ "Splash should not be re-extracted if jar hasn't changed" );
2919+
2920+ // Update jar timestamp to force re-extraction
2921+ Files .setLastModifiedTime (jar ,
2922+ java .nio .file .attribute .FileTime .fromMillis (System .currentTimeMillis () + 10000 ));
2923+
2924+ // Third run - should re-extract splash
2925+ CommandLine .ParseResult pr3 = JBang .getCommandLine ().parseArgs ("run" , jar .toString ());
2926+ Run run3 = (Run ) pr3 .subcommand ().commandSpec ().userObject ();
2927+ ProjectBuilder pb3 = run3 .createProjectBuilderForRun ();
2928+ Project code3 = pb3 .build (jar .toString ());
2929+ String cmd3 = CmdGenerator .builder (code3 ).build ().generate ();
2930+
2931+ java .nio .file .attribute .FileTime thirdTimestamp = Files .getLastModifiedTime (extractedPath );
2932+ assertTrue (thirdTimestamp .compareTo (secondTimestamp ) > 0 ,
2933+ "Splash should be re-extracted when jar is newer" );
2934+ }
2935+
2936+ @ Test
2937+ void testAddReadsMalformed (@ TempDir Path output ) throws IOException {
2938+ assumeTrue (Runtime .version ().feature () >= 9 , "requires Java 9+" );
2939+ // Test with spaces in Add-Reads - should generate multiple flags
2940+ String reads = "java.base=java.logging module.a=module.b" ;
2941+ Path jar = createJar (output , Integer .toString (Runtime .version ().feature ()),
2942+ Collections .singletonMap (Project .ATTR_ADD_READS , reads ));
2943+
2944+ CommandLine .ParseResult pr = JBang .getCommandLine ().parseArgs ("run" , jar .toString ());
2945+ Run run = (Run ) pr .subcommand ().commandSpec ().userObject ();
2946+
2947+ ProjectBuilder pb = run .createProjectBuilderForRun ();
2948+ Project code = pb .build (jar .toString ());
2949+ String cmd = CmdGenerator .builder (code ).build ().generate ();
2950+
2951+ // Should handle extra whitespace gracefully
2952+ assertThat (cmd , containsString ("--add-reads=java.base=java.logging" ));
2953+ assertThat (cmd , containsString ("--add-reads=module.a=module.b" ));
2954+ }
2955+
28092956 private Path createJar (Path outputDir , String buildJdk , Map <String , String > manifestAttributes ) throws IOException {
28102957 Path jar = outputDir .resolve ("manifest-test.jar" );
28112958 Manifest manifest = new Manifest ();
0 commit comments