diff --git a/src/java/containers/container.go b/src/java/containers/container.go index e16fefe05..06a7b84b7 100644 --- a/src/java/containers/container.go +++ b/src/java/containers/container.go @@ -127,6 +127,19 @@ func (r *Registry) RegisterStandardContainers() { r.Register(NewJavaMainContainer(r.context)) } +// JavaExecCommand builds a start command of the form: +// +// eval "exec $JAVA_HOME/bin/java $JAVA_OPTS " +// +// Wrapping the argument to eval in double quotes prevents bash from +// glob-expanding or word-splitting $JAVA_OPTS before eval sees it. +// eval then re-parses the string, honouring any embedded quotes in $JAVA_OPTS. +// javaArgs may include app/config-derived values (e.g. jar paths, Main-Class, +// configured arguments), so callers must ensure it is shell-safe. +func JavaExecCommand(javaArgs string) string { + return `eval "exec $JAVA_HOME/bin/java $JAVA_OPTS ` + javaArgs + `"` +} + // This script is used to process the CLASSPATH assembled from various framework scripts sourced from profile.d // to further create symlinks to the corresponding framework dependencies in WEB-INF/lib, BOOT-INF/lib and where ever // needed thus they are available for application classloading diff --git a/src/java/containers/java_main.go b/src/java/containers/java_main.go index 99570f339..08b6ceb55 100644 --- a/src/java/containers/java_main.go +++ b/src/java/containers/java_main.go @@ -286,19 +286,18 @@ func (j *JavaMainContainer) Release() (string, error) { args := "" if cfg.Arguments != "" { - args = " " + cfg.Arguments + // Escape double quotes so they don't terminate the outer eval "..." string. + args = " " + strings.ReplaceAll(cfg.Arguments, `"`, `\"`) } // JBP_CONFIG_JAVA_MAIN java_main_class takes precedence over manifest Main-Class. // Use classpath mode so the configured class is actually invoked (not the manifest's). if cfg.JavaMainClass != "" { - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", cfg.JavaMainClass, args), nil + return JavaExecCommand(fmt.Sprintf("-cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", cfg.JavaMainClass, args)), nil } if j.jarFile != "" { - // JAR has its own Main-Class in the manifest — java -jar handles it - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -jar %s%s", j.jarFile, args), nil + return JavaExecCommand(fmt.Sprintf("-jar %s%s", j.jarFile, args)), nil } // Classpath mode: need an explicit main class @@ -311,6 +310,5 @@ func (j *JavaMainContainer) Release() (string, error) { j.context.Log.Debug("Main Class %s found in JAVA_MAIN_CLASS", mainClass) } - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", mainClass, args), nil + return JavaExecCommand(fmt.Sprintf("-cp ${CLASSPATH}${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s%s", mainClass, args)), nil } diff --git a/src/java/containers/java_main_test.go b/src/java/containers/java_main_test.go index 2a0d0f0cd..a4dec30d5 100644 --- a/src/java/containers/java_main_test.go +++ b/src/java/containers/java_main_test.go @@ -154,6 +154,10 @@ var _ = Describe("Java Main Container", func() { }) Describe("Release", func() { + expectQuotedEval := func(cmd string) { + Expect(cmd).To(MatchRegexp(`eval "exec .*\$JAVA_OPTS`)) + } + Context("with JAR file", func() { BeforeEach(func() { Expect(createJar( @@ -278,6 +282,77 @@ var _ = Describe("Java Main Container", func() { Expect(err).NotTo(HaveOccurred()) Expect(cmd).To(ContainSubstring("--foo=bar")) }) + + It("escapes double quotes in arguments to avoid breaking the eval string", func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", `{arguments: "--spring.datasource.url=\"jdbc:h2:mem:test\""}`) + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + // Double quotes in arguments must be backslash-escaped so they + // don't terminate the outer eval "..." string. + Expect(cmd).To(ContainSubstring(`--spring.datasource.url=\"jdbc:h2:mem:test\"`)) + }) + }) + + // Regression tests for issue #1301: start command must use eval "exec ... $JAVA_OPTS" + // (quoted string) so that glob chars in JAVA_OPTS are not expanded by bash before eval. + Context("with JAR file (eval quoting)", func() { + BeforeEach(func() { + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) + + Context("with JAVA_MAIN_CLASS env variable (eval quoting)", func() { + BeforeEach(func() { + os.Setenv("JAVA_MAIN_CLASS", "com.example.Main") + os.WriteFile(filepath.Join(buildDir, "Main.class"), []byte("fake"), 0644) + container.Detect() + }) + + AfterEach(func() { + os.Unsetenv("JAVA_MAIN_CLASS") + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) + }) + + Context("with JBP_CONFIG_JAVA_MAIN java_main_class (eval quoting)", func() { + BeforeEach(func() { + os.Setenv("JBP_CONFIG_JAVA_MAIN", "{java_main_class: com.example.Main}") + Expect(createJar( + filepath.Join(buildDir, "app.jar"), + "Manifest-Version: 1.0\nMain-Class: com.example.Main\n", + )).To(Succeed()) + container.Detect() + }) + + AfterEach(func() { + os.Unsetenv("JBP_CONFIG_JAVA_MAIN") + }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) }) Context("without main class or JAR", func() { @@ -351,6 +426,7 @@ var _ = Describe("Java Main Container", func() { Expect(cmd).To(ContainSubstring(".")) }) }) + }) Describe("Finalize", func() { diff --git a/src/java/containers/play.go b/src/java/containers/play.go index 2dc37717a..3001c6d7b 100644 --- a/src/java/containers/play.go +++ b/src/java/containers/play.go @@ -548,8 +548,8 @@ func (p *PlayContainer) Release() (string, error) { libPath = filepath.ToSlash(relPath) } } - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - cmd = fmt.Sprintf("eval exec java $JAVA_OPTS -cp $HOME/%s/* play.core.server.NettyServer $HOME", libPath) + // Use quoted eval argument to prevent glob/word-splitting of $JAVA_OPTS before eval. + cmd = fmt.Sprintf(`eval "exec java $JAVA_OPTS -cp $HOME/%s/* play.core.server.NettyServer $HOME"`, libPath) } p.context.Log.Debug("Play Framework release command: %s", cmd) diff --git a/src/java/containers/play_test.go b/src/java/containers/play_test.go index d4d225434..6d758afe3 100644 --- a/src/java/containers/play_test.go +++ b/src/java/containers/play_test.go @@ -185,7 +185,7 @@ var _ = Describe("Play Container", func() { It("returns correct java command", func() { cmd, err := container.Release() Expect(err).NotTo(HaveOccurred()) - Expect(cmd).To(ContainSubstring("java $JAVA_OPTS -cp")) + Expect(cmd).To(ContainSubstring(`eval "exec java $JAVA_OPTS -cp`)) Expect(cmd).To(ContainSubstring("play.core.server.NettyServer $HOME")) }) }) @@ -201,7 +201,7 @@ var _ = Describe("Play Container", func() { It("returns correct java command", func() { cmd, err := container.Release() Expect(err).NotTo(HaveOccurred()) - Expect(cmd).To(ContainSubstring("java $JAVA_OPTS -cp")) + Expect(cmd).To(ContainSubstring(`eval "exec java $JAVA_OPTS -cp`)) Expect(cmd).To(ContainSubstring("play.core.server.NettyServer $HOME")) }) }) diff --git a/src/java/containers/spring_boot.go b/src/java/containers/spring_boot.go index ac6119c1c..e7a693c61 100644 --- a/src/java/containers/spring_boot.go +++ b/src/java/containers/spring_boot.go @@ -256,21 +256,16 @@ func (s *SpringBootContainer) Release() (string, error) { // Verify this is actually a Spring Boot application if s.isSpringBootExplodedJar(buildDir) { - // True Spring Boot exploded JAR - use main class from manifest or fallback to JarLauncher based on spring-boot version launcherClass := s.getLauncherClass(buildDir) - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", launcherClass), nil + return JavaExecCommand(fmt.Sprintf("-cp $PWD/.${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER} %s", launcherClass)), nil } - // Exploded JAR but NOT Spring Boot - use Main-Class from MANIFEST.MF mainClass, err := s.readMainClassFromManifest(buildDir) if err != nil { s.context.Log.Debug("Could not read MANIFEST.MF: %s", err.Error()) } if mainClass != "" { - // Use classpath from BOOT-INF/classes and BOOT-INF/lib - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - return fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS -cp $HOME${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass), nil + return JavaExecCommand(fmt.Sprintf("-cp $HOME${CONTAINER_SECURITY_PROVIDER:+:$CONTAINER_SECURITY_PROVIDER}:$HOME/BOOT-INF/classes:$HOME/BOOT-INF/lib/* %s", mainClass)), nil } return "", fmt.Errorf("exploded JAR found but no Main-Class in MANIFEST.MF") @@ -292,8 +287,8 @@ func (s *SpringBootContainer) Release() (string, error) { jarFile = jar } - // Use eval to properly handle backslash-escaped values in $JAVA_OPTS (Ruby buildpack parity) - cmd := fmt.Sprintf("eval exec $JAVA_HOME/bin/java $JAVA_OPTS ${CONTAINER_SECURITY_PROVIDER:+-Dloader.path=$CONTAINER_SECURITY_PROVIDER} -jar %s", jarFile) + // Use eval with quoted string to prevent glob-expansion of $JAVA_OPTS (#1301) + cmd := JavaExecCommand(fmt.Sprintf("${CONTAINER_SECURITY_PROVIDER:+-Dloader.path=$CONTAINER_SECURITY_PROVIDER} -jar %s", jarFile)) return cmd, nil } diff --git a/src/java/containers/spring_boot_test.go b/src/java/containers/spring_boot_test.go index 752dd1ec7..f3239a7ce 100644 --- a/src/java/containers/spring_boot_test.go +++ b/src/java/containers/spring_boot_test.go @@ -122,6 +122,10 @@ var _ = Describe("Spring Boot Container", func() { }) Describe("Release", func() { + expectQuotedEval := func(cmd string) { + Expect(cmd).To(MatchRegexp(`eval "exec .*\$JAVA_OPTS`)) + } + Context("with exploded JAR (BOOT-INF)", func() { BeforeEach(func() { os.MkdirAll(filepath.Join(buildDir, "BOOT-INF"), 0755) @@ -136,6 +140,12 @@ var _ = Describe("Spring Boot Container", func() { Expect(err).NotTo(HaveOccurred()) Expect(cmd).To(ContainSubstring("JarLauncher")) }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) }) Context("with Spring Boot JAR", func() { @@ -151,6 +161,12 @@ var _ = Describe("Spring Boot Container", func() { Expect(cmd).To(ContainSubstring("java")) Expect(cmd).To(ContainSubstring("app-boot.jar")) }) + + It("uses quoted eval to protect $JAVA_OPTS from glob expansion", func() { + cmd, err := container.Release() + Expect(err).NotTo(HaveOccurred()) + expectQuotedEval(cmd) + }) }) Context("with no Spring Boot JAR found", func() { diff --git a/src/java/frameworks/java_opts_writer.go b/src/java/frameworks/java_opts_writer.go index 4bfd0a6d1..66db45f8b 100644 --- a/src/java/frameworks/java_opts_writer.go +++ b/src/java/frameworks/java_opts_writer.go @@ -74,30 +74,51 @@ func CreateJavaOptsAssemblyScript(ctx *common.Context) error { # Save original JAVA_OPTS from environment (user-provided) # Normalize to single line: YAML block scalars (>) may introduce newlines -# xargs trims leading/trailing whitespace and collapses internal spaces -USER_JAVA_OPTS=$(echo "$JAVA_OPTS" | tr '\n' ' ' | tr -s ' ' | xargs) +# Only convert newlines to spaces — do not use xargs which strips quotes and backslashes +USER_JAVA_OPTS=$(printf '%%s' "$JAVA_OPTS" | tr '\n' ' ') # Start building new JAVA_OPTS JAVA_OPTS="" +# Escape replacement-special chars once; these values are loop-invariant. +_escaped_deps_dir="${DEPS_DIR//\\/\\\\}" +_escaped_deps_dir="${_escaped_deps_dir//&/\\&}" +_escaped_home="${HOME//\\/\\\\}" +_escaped_home="${_escaped_home//&/\\&}" +_user_java_opts_placeholder='__JAVA_OPTS_BUILDPACK_PLACEHOLDER__' +_escaped_user_java_opts="${USER_JAVA_OPTS//\\/\\\\}" +_escaped_user_java_opts="${_escaped_user_java_opts//&/\\&}" + if [ -d "$DEPS_DIR/%s/java_opts" ]; then for opts_file in "$DEPS_DIR/%s/java_opts"/*.opts; do if [ -f "$opts_file" ]; then # Read content and expand runtime variables opts_content=$(cat "$opts_file") - # Expand $DEPS_DIR, $HOME, $JAVA_OPTS using bash parameter expansion. - # sed-based substitution breaks when these values contain the sed delimiter (|), - # backslashes, ampersands, or newlines — all valid in JAVA_OPTS and paths. - opts_content="${opts_content//\$DEPS_DIR/$DEPS_DIR}" - opts_content="${opts_content//\$HOME/$HOME}" - opts_content="${opts_content//\$JAVA_OPTS/$USER_JAVA_OPTS}" - + # Expand $DEPS_DIR and $HOME using bash parameter expansion. + # In ${var//pattern/repl}, '&' and '\' are special in replacement strings, + # so escape them first to preserve literal path contents. + opts_content="${opts_content//\$DEPS_DIR/$_escaped_deps_dir}" + opts_content="${opts_content//\$HOME/$_escaped_home}" + + # Shield $JAVA_OPTS from eval: replace with a placeholder first, + # then substitute the actual value AFTER eval so that quotes and + # backslashes in the user-provided JAVA_OPTS are never exposed to eval. + opts_content="${opts_content//\$JAVA_OPTS/$_user_java_opts_placeholder}" + + # Escape \ and " in opts_content so they do not break the + # eval "..." wrapper below. \ must be escaped first. + _eval_safe="${opts_content//\\/\\\\}" + _eval_safe="${_eval_safe//\"/\\\"}" + # Expand any remaining environment variables in opts content via eval. # Note: eval executes commands, but .opts files are written by the buildpack # at staging time and run within the container context. # This matches how the Ruby buildpack naturally expanded variables via shell. - opts_content=$(eval "echo \"$opts_content\"") + opts_content=$(eval "printf '%%s' \"$_eval_safe\"") + + # Now safely substitute JAVA_OPTS after eval (preserves quotes, backslashes, and ampersands) + opts_content="${opts_content//$_user_java_opts_placeholder/$_escaped_user_java_opts}" if [ -n "$opts_content" ]; then JAVA_OPTS="$JAVA_OPTS $opts_content" diff --git a/src/java/frameworks/java_opts_writer_test.go b/src/java/frameworks/java_opts_writer_test.go index e092ec5df..b97f12f57 100644 --- a/src/java/frameworks/java_opts_writer_test.go +++ b/src/java/frameworks/java_opts_writer_test.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -53,17 +54,8 @@ var _ = Describe("Java Opts Writer", func() { os.RemoveAll(depsDir) }) - Describe("Basic options", func() { - It("writes JAVA_OPTS correctly", func() { - javaOpts := "-Xmx512M -Xms256M" - os.Setenv("JAVA_OPTS", javaOpts) - - Expect(os.Getenv("JAVA_OPTS")).To(Equal(javaOpts)) - }) - }) - Describe("CreateJavaOptsAssemblyScript", func() { - runScript := func(javaOpts string, optsFileContent string) (string, error) { + setupScript := func(_ string, optsFileContent string) string { err := frameworks.CreateJavaOptsAssemblyScript(ctx) Expect(err).NotTo(HaveOccurred()) @@ -71,8 +63,11 @@ var _ = Describe("Java Opts Writer", func() { Expect(os.MkdirAll(optsDir, 0755)).To(Succeed()) Expect(os.WriteFile(filepath.Join(optsDir, "42_agent.opts"), []byte(optsFileContent), 0644)).To(Succeed()) - scriptPath := filepath.Join(depsDir, "0", "profile.d", "00_java_opts.sh") - cmd := exec.Command("bash", "-c", "source "+scriptPath+" && echo \"$JAVA_OPTS\"") + return filepath.Join(depsDir, "0", "profile.d", "00_java_opts.sh") + } + + runWithEnv := func(scriptPath, javaOpts, bashExpr string) (string, error) { + cmd := exec.Command("bash", "-c", "source "+scriptPath+" && "+bashExpr) cmd.Env = append(os.Environ(), "JAVA_OPTS="+javaOpts, "DEPS_DIR="+depsDir, @@ -82,6 +77,33 @@ var _ = Describe("Java Opts Writer", func() { return string(output), err } + runWithCustomRuntimeEnv := func(scriptPath, javaOpts, bashExpr, runtimeDepsDir, runtimeHome string) (string, error) { + cmd := exec.Command("bash", "-c", "source "+scriptPath+" && "+bashExpr) + cmd.Env = append(os.Environ(), + "JAVA_OPTS="+javaOpts, + "DEPS_DIR="+runtimeDepsDir, + "HOME="+runtimeHome, + ) + output, err := cmd.CombinedOutput() + return string(output), err + } + + runScript := func(javaOpts string, optsFileContent string) (string, error) { + scriptPath := setupScript(javaOpts, optsFileContent) + return runWithEnv(scriptPath, javaOpts, `printf '%s\n' "$JAVA_OPTS"`) + } + + // runStartCommand simulates the actual JVM invocation: + // eval "exec $JAVA_HOME/bin/java $JAVA_OPTS -jar app.jar" + // Returns the argument list java would receive (one arg per line). + runStartCommand := func(javaOpts string, optsFileContent string) (string, error) { + scriptPath := setupScript(javaOpts, optsFileContent) + // Simulate: eval "exec java $JAVA_OPTS" — quoted string prevents bash glob-expansion. + // eval then re-parses the string, honouring embedded quotes in $JAVA_OPTS. + return runWithEnv(scriptPath, javaOpts, + `eval "set -- $JAVA_OPTS"; printf '%s\n' "$@"`) + } + It("handles multiline JAVA_OPTS from YAML block scalar without sed error", func() { // Reproduce the manifest pattern: // JAVA_OPTS: > @@ -112,10 +134,107 @@ var _ = Describe("Java Opts Writer", func() { Expect(output).To(ContainSubstring("-javaagent:/home/vcap/app/BOOT-INF/lib/agent.jar")) }) + It("preserves ampersand and backslash in $HOME replacement", func() { + scriptPath := setupScript("", "-javaagent:$HOME/BOOT-INF/lib/agent.jar") + customHome := `/tmp/home&dir\sub` + output, err := runWithCustomRuntimeEnv(scriptPath, "", `printf '%s\n' "$JAVA_OPTS"`, depsDir, customHome) + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring("-javaagent:" + customHome + "/BOOT-INF/lib/agent.jar")) + }) + It("expands $DEPS_DIR in opts file content", func() { output, err := runScript("", "-Djava.security.properties=$DEPS_DIR/0/security.properties") Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) Expect(output).To(ContainSubstring("-Djava.security.properties=" + depsDir + "/0/security.properties")) }) + + It("preserves literal -n from opts file content", func() { + output, err := runScript("", "-n") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(strings.TrimSpace(output)).To(Equal("-n")) + }) + + // Regression tests for issue #1301: xargs strips quotes, breaking quoted JVM args + It("preserves quoted value with spaces in JAVA_OPTS", func() { + // JAVA_OPTS='-Dfoo="bar baz"' — xargs removes the quotes from USER_JAVA_OPTS, + // so when eval exec java $JAVA_OPTS is called, -Dfoo=bar and baz become separate args + output, err := runScript(`-Dfoo="bar baz"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dfoo="bar baz"`)) + }) + + It("preserves cron expression with glob characters in JAVA_OPTS", func() { + // JAVA_OPTS='-DcronSched="0 */7 * * * *"' — xargs strips quotes, then * expands via glob + // when eval exec java $JAVA_OPTS is invoked, corrupting the cron expression + output, err := runScript(`-DcronSched="0 */7 * * * *"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-DcronSched="0 */7 * * * *"`)) + }) + + It("preserves multiple quoted args in JAVA_OPTS", func() { + // Multiple quoted values — xargs strips all quotes, each space-containing value splits + output, err := runScript(`-Dfoo="bar baz" -Dother="qux quux"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dfoo="bar baz"`)) + Expect(output).To(ContainSubstring(`-Dother="qux quux"`)) + }) + + It("preserves backslashes in JAVA_OPTS values", func() { + // xargs treats backslash as escape char: C:\path\to\app -> C:pathtoapp + // Affects regex patterns and any path using backslash notation + output, err := runScript(`-DregEx="[a-z]+(.*)" -Dpattern=foo\|bar`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-DregEx="[a-z]+(.*)"`)) + Expect(output).To(ContainSubstring(`foo\|bar`)) + }) + + It("preserves leading -n argument in JAVA_OPTS", func() { + output, err := runScript(`-n -Dfoo=bar`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-n -Dfoo=bar`)) + }) + + It("preserves ampersand in JAVA_OPTS values during placeholder substitution", func() { + output, err := runScript(`-Dfoo=a&b`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dfoo=a&b`)) + }) + + It("preserves backslashes in JAVA_OPTS values during placeholder substitution", func() { + output, err := runScript(`-Dpath=C:\tmp\app`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dpath=C:\tmp\app`)) + }) + + It("preserves double backslashes in JAVA_OPTS values during placeholder substitution", func() { + output, err := runScript(`-Dpath=C:\\double`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dpath=C:\\double`)) + }) + + // Full invocation cycle test for issue #1301: + // Verifies that the quoted eval "exec ... $JAVA_OPTS" form delivers the correct + // argument to java — glob chars in $JAVA_OPTS are not expanded. + It("does not glob-expand * in cron expression when invoking java", func() { + output, err := runStartCommand(`-DcronSched="0 */7 * * * *"`, "$JAVA_OPTS") + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + // Java receives exactly one arg: -DcronSched=0 */7 * * * * + Expect(strings.TrimSpace(output)).To(Equal("-DcronSched=0 */7 * * * *")) + }) + + // Regression test: eval mangles .opts content containing literal double quotes. + // e.g. Datadog writes -Ddd.service="myapp" into its .opts file; the inner " + // terminates the outer eval "..." string, stripping the value. + It("preserves double-quoted values from .opts file content", func() { + output, err := runScript("", `-Ddd.service="myapp"`) + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Ddd.service="myapp"`)) + }) + + It("preserves backslashes in .opts file content", func() { + output, err := runScript("", `-Dpattern=foo\|bar`) + Expect(err).NotTo(HaveOccurred(), "script failed with output: %s", output) + Expect(output).To(ContainSubstring(`-Dpattern=foo\|bar`)) + }) }) })