Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.Set;

import com.fortify.cli.common.exception.FcliSimpleException;
import com.fortify.cli.common.util.WindowsPathValidator;

/**
* Shared option validation for SSC/FoD apply-remediations commands (CLI surface only).
Expand All @@ -26,6 +27,7 @@ private AviatorApplyRemediationsCliSupport() {}
public static void requireSourceDir(String sourceCodeDirectory) {
FcliSimpleException.throwIf(sourceCodeDirectory == null || sourceCodeDirectory.isBlank(),
"--source-dir must specify a valid directory path");
WindowsPathValidator.validate("--source-dir", sourceCodeDirectory);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.fortify.cli.aviator.ssc.cli.mixin.SSCRemediationsSelectionMode;
import com.fortify.cli.aviator.ssc.helper.SinceOptionHelper;
import com.fortify.cli.common.cli.mixin.CommonOptionMixins;
import com.fortify.cli.common.cli.util.WindowsFileConverter;
import com.fortify.cli.common.json.JsonHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
Expand All @@ -53,7 +54,8 @@ public class AviatorSSCDownloadRemediationsCacheCommand extends AbstractSSCJsonN
@Mixin private AviatorSSCRemediationsCacheDownloadSelectorMixin artifactSelector;
@Mixin private CommonOptionMixins.RequireConfirmation requireConfirmation;

@Option(names = {"-f", "--file"}, required = true, paramLabel = "<file>")
@Option(names = {"-f", "--file"}, required = true, paramLabel = "<file>",
converter = WindowsFileConverter.class)
private File outputFile;

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.nio.file.Path;

import com.fortify.cli.aviator.ssc.cli.mixin.AviatorSSCRemediationsSelectorArgGroups.OnlineSelectionArgGroup;
import com.fortify.cli.common.cli.util.WindowsPathConverter;
import com.fortify.cli.common.exception.FcliSimpleException;

import lombok.Getter;
Expand All @@ -38,7 +39,8 @@ public static class SourceArgGroup {

/** Shared/arg-group option: keep descriptionKey (default picocli key uses FQCN). */
@Option(names = {"--from-cache"}, required = true, paramLabel = "<zip>",
descriptionKey = "fcli.aviator.ssc.apply-remediations.from-cache")
descriptionKey = "fcli.aviator.ssc.apply-remediations.from-cache",
converter = WindowsPathConverter.class)
private Path fromCache;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import com.fortify.cli.aviator.ssc.cli.mixin.AviatorSSCApplyRemediationsSourceMixin;
import com.fortify.cli.common.exception.FcliSimpleException;
Expand Down Expand Up @@ -50,6 +52,22 @@ void issueIdsRequireFromCache() {
assertThrows(FcliSimpleException.class, command::getJsonNode);
}

@Test
@EnabledOnOs(OS.WINDOWS)
void sourceDirRejectsCorruptionMarker() {
AviatorSSCApplyRemediationsCommand command = parse(
"--artifact-id", "1", "--source-dir", "C:\\temp\\?\\source");

assertThrows(FcliSimpleException.class, command::getJsonNode);
}

@Test
@EnabledOnOs(OS.WINDOWS)
void fromCacheRejectsCorruptionMarker() {
assertThrows(CommandLine.ParameterException.class,
() -> parse("--from-cache", "C:\\temp\\?\\cache.zip"));
}

private static AviatorSSCApplyRemediationsCommand parse(String... args) {
AviatorSSCApplyRemediationsCommand command = new AviatorSSCApplyRemediationsCommand();
new CommandLine(command).parseArgs(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import com.fortify.cli.common.exception.FcliSimpleException;

Expand All @@ -40,6 +42,13 @@ void testArtifactIdRejectsAppVersion() {
assertThrows(FcliSimpleException.class, () -> command.getJsonNode(null));
}

@Test
@EnabledOnOs(OS.WINDOWS)
void fileRejectsCorruptionMarker() {
assertThrows(CommandLine.ParameterException.class,
() -> parse("--artifact-id", "1", "-f", "C:\\temp\\?\\cache.zip"));
}

private static AviatorSSCDownloadRemediationsCacheCommand parse(String... args) {
AviatorSSCDownloadRemediationsCacheCommand command = new AviatorSSCDownloadRemediationsCacheCommand();
new CommandLine(command).parseArgs(args);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.common.cli.util;

import java.io.File;

import com.fortify.cli.common.util.WindowsPathValidator;

import picocli.CommandLine.ITypeConverter;

public final class WindowsFileConverter implements ITypeConverter<File> {
@Override
public File convert(String value) {
return WindowsPathValidator.toPath("The supplied path", value).toFile();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.common.cli.util;

import java.nio.file.Path;

import com.fortify.cli.common.util.WindowsPathValidator;

import picocli.CommandLine.ITypeConverter;

public final class WindowsPathConverter implements ITypeConverter<Path> {
@Override
public Path convert(String value) {
return WindowsPathValidator.toPath("The supplied path", value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.common.util;

import java.nio.file.InvalidPathException;
import java.nio.file.Path;

import com.fortify.cli.common.exception.FcliSimpleException;

/** Validates path values that may have been corrupted by Windows command-line decoding. */
public final class WindowsPathValidator {
private static final char REPLACEMENT_CHARACTER = '\uFFFD';

private WindowsPathValidator() {}

public static boolean hasUnsupportedCharacters(String value) {
return PlatformHelper.isWindows()
&& value != null
&& (value.indexOf('?') >= 0 || value.indexOf(REPLACEMENT_CHARACTER) >= 0);
}

public static void validate(String optionName, String value) {
FcliSimpleException.throwIf(
hasUnsupportedCharacters(value),
"%s contains '?' or the Unicode replacement character (U+FFFD). "
+ "This may indicate corruption during Windows command-line decoding; "
+ "please rerun the command with a valid Windows path",
optionName);
}

public static Path toPath(String optionName, String value) {
validate(optionName, value);
try {
return Path.of(value);
} catch (InvalidPathException e) {
throw new FcliSimpleException(
"%s is not a valid file path on this platform; please provide a supported file path",
optionName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2021-2026 Open Text.
*
* The only warranties for products and services of Open Text
* and its affiliates and licensors ("Open Text") are as may
* be set forth in the express warranty statements accompanying
* such products and services. Nothing herein should be construed
* as constituting an additional warranty. Open Text shall not be
* liable for technical or editorial errors or omissions contained
* herein. The information contained herein is subject to change
* without notice.
*/
package com.fortify.cli.common.util;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import com.fortify.cli.common.cli.util.WindowsFileConverter;
import com.fortify.cli.common.cli.util.WindowsPathConverter;
import com.fortify.cli.common.exception.FcliSimpleException;

class WindowsPathValidatorTest {
private static final String CORRUPTED_PATH = "C:\\temp\\?\\source";
private static final String REPLACEMENT_PATH = "C:\\temp\\\uFFFD\\source";

@Test
@EnabledOnOs(OS.WINDOWS)
void rejectsQuestionMarkPath() {
assertTrue(WindowsPathValidator.hasUnsupportedCharacters(CORRUPTED_PATH));
assertThrows(FcliSimpleException.class, () -> new WindowsPathConverter().convert(CORRUPTED_PATH));
assertThrows(FcliSimpleException.class, () -> new WindowsFileConverter().convert(CORRUPTED_PATH));
}

@Test
@EnabledOnOs(OS.WINDOWS)
void rejectsReplacementCharacterPath() {
assertTrue(WindowsPathValidator.hasUnsupportedCharacters(REPLACEMENT_PATH));
FcliSimpleException exception = assertThrows(
FcliSimpleException.class, () -> WindowsPathValidator.validate("--source-dir", REPLACEMENT_PATH));
assertTrue(exception.getMessage().contains("Windows command-line decoding"));
}

@Test
@EnabledOnOs(OS.WINDOWS)
void acceptsUnicodePathWithoutCorruptionMarkers() {
assertDoesNotThrow(() -> WindowsPathValidator.validate("--source-dir", "C:\\temp\\Fortify-test\\source"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.nio.file.Path;

import com.fortify.cli.common.cli.util.WindowsPathConverter;
import com.fortify.cli.fod._common.cli.mixin.FoDDelimiterMixin;
import com.fortify.cli.fod._common.cli.mixin.IFoDDelimiterMixinAware;
import com.fortify.cli.fod.release.cli.mixin.FoDReleaseByQualifiedNameOrIdResolverMixin;
Expand Down Expand Up @@ -59,7 +60,8 @@ static class SourceArgGroup {

/** Shared description key: command-local option on an ArgGroup (default picocli key would use FQCN). */
@Option(names = {"--from-cache"}, required = true, paramLabel = "<zip>",
descriptionKey = "fcli.fod.aviator.apply-remediations.from-cache")
descriptionKey = "fcli.fod.aviator.apply-remediations.from-cache",
converter = WindowsPathConverter.class)
private Path fromCache;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.fortify.cli.aviator._common.remediations_cache.RemediationsCacheWriter;
import com.fortify.cli.aviator.config.AviatorLoggerImpl;
import com.fortify.cli.common.cli.mixin.CommonOptionMixins;
import com.fortify.cli.common.cli.util.WindowsFileConverter;
import com.fortify.cli.common.json.JsonHelper;
import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins;
import com.fortify.cli.common.output.transform.IActionCommandResultSupplier;
Expand All @@ -50,7 +51,8 @@ public class FoDAviatorDownloadRemediationsCacheCommand extends AbstractFoDJsonN
@Mixin private FoDReleaseByQualifiedNameOrIdResolverMixin.RequiredOption releaseResolver;
@Mixin private CommonOptionMixins.RequireConfirmation requireConfirmation;

@Option(names = {"-f", "--file"}, required = true, paramLabel = "<file>")
@Option(names = {"-f", "--file"}, required = true, paramLabel = "<file>",
converter = WindowsFileConverter.class)
private File outputFile;

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import com.fortify.cli.common.exception.FcliSimpleException;
import com.fortify.cli.fod.aviator.cli.mixin.FoDAviatorApplyRemediationsSourceMixin;
Expand Down Expand Up @@ -64,6 +66,22 @@ void blankSourceDirIsRejected() throws Exception {
assertThrows(FcliSimpleException.class, command::getJsonNode);
}

@Test
@EnabledOnOs(OS.WINDOWS)
void sourceDirRejectsCorruptionMarker() {
FoDAviatorApplyRemediationsCommand command = parse(
"--release", "1", "--source-dir", "C:\\temp\\?\\source");

assertThrows(FcliSimpleException.class, command::getJsonNode);
}

@Test
@EnabledOnOs(OS.WINDOWS)
void fromCacheRejectsCorruptionMarker() {
assertThrows(CommandLine.ParameterException.class,
() -> parse("--from-cache", "C:\\temp\\?\\cache.zip"));
}

private static FoDAviatorApplyRemediationsCommand parse(String... args) {
FoDAviatorApplyRemediationsCommand command = new FoDAviatorApplyRemediationsCommand();
new CommandLine(command).parseArgs(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import com.fortify.cli.fod.aviator.cmd.FoDAviatorDownloadRemediationsCacheCommand;

Expand All @@ -33,6 +35,13 @@ void fileIsRequired() {
() -> parse("--release", "1"));
}

@Test
@EnabledOnOs(OS.WINDOWS)
void fileRejectsCorruptionMarker() {
assertThrows(CommandLine.ParameterException.class,
() -> parse("--release", "1", "-f", "C:\\temp\\?\\cache.zip"));
}

private static void parse(String... args) {
new CommandLine(new FoDAviatorDownloadRemediationsCacheCommand()).parseArgs(args);
}
Expand Down
Loading