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 @@ -119,16 +119,20 @@ public RemediationMetric processRemediationXML() {


if (!calculateHashBase64(content, "SHA-256").equals(fileHash)) {
logger.trace("File hash mismatch for remediation {} in {}; searching changed source content", instanceId, filename);
Element contextElem = (Element) change.getElementsByTagNameNS(NAMESPACE_URI, "Context").item(0);
String contextText = contextElem.getTextContent();

//spliting a string into a list of lines, using both Unix (\n) and Windows (\r\n) line endings.
List<String> contextLine = Arrays.asList(contextText.split("\\r?\\n"));
int contextLineFrom = FuzzyContextSearcher.fuzzySearchContext(originalLines, contextLine, 0) ;
if(contextLineFrom==-1) {
logger.trace("Context search failed for remediation {} in {}; context lines={}, source lines={}", instanceId, filename,
contextLine.size(), originalLines.size());
logger.info("File content has changed. Context Lines not found. Remediation not possible for {}", instanceId);
continue;
}
logger.trace("Context for remediation {} in {} matched at line {}", instanceId, filename, contextLineFrom + 1);
Element OriginalCodeElem = (Element) change.getElementsByTagNameNS(NAMESPACE_URI, "OriginalCode").item(0);
String OriginalCodeText = OriginalCodeElem.getTextContent();

Expand All @@ -137,11 +141,14 @@ public RemediationMetric processRemediationXML() {

int[] lineFromTo = FuzzyContextSearcher.fuzzySearchOriginalCode(originalLines, OriginalCodeLine, 0, contextLineFrom);
if(lineFromTo[0]==-1 || lineFromTo[1]==-1) {
logger.trace("Original code search failed for remediation {} in {}; context line={}, original code lines={}, source lines={}",
instanceId, filename, contextLineFrom + 1, OriginalCodeLine.size(), originalLines.size());
logger.info("File content has changed. Original Code lines not found. Remediation not possible for {}", instanceId);
continue;
}
lineFrom = lineFromTo[0]+1; //Adding 1 for 1-based indexing
lineTo = lineFromTo[1] + 1; //Adding 1 for 1-based indexing
logger.trace("Original code for remediation {} in {} matched at lines {}-{}", instanceId, filename, lineFrom, lineTo);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public final class FprHandle implements AutoCloseable {
*/
@Getter
private final Path fprPath;
private final Map<String, String> sourceFileMap;
private volatile Map<String, String> sourceFileMap;


/**
Expand All @@ -75,7 +75,6 @@ public FprHandle(Path fprPath) {
} catch (IOException e) {
throw new AviatorTechnicalException("Failed to open FPR as a zip file system: " + fprPath, e);
}
this.sourceFileMap = loadSourceFileMap();
}

/**
Expand All @@ -102,6 +101,7 @@ public void validate() {
if (!hasSource()) {
throw new AviatorSimpleException("Invalid FPR: Source code is missing or incomplete. The 'src-archive' directory must contain 'index.xml' and at least one source file.");
}
getSourceFileMap();
LOG.info("FPR validation successful for: {}", this.fprPath);
}

Expand Down Expand Up @@ -145,7 +145,12 @@ public void close() throws IOException {
* Returns the map of relative source file paths to their paths within the FPR archive.
* @return A map of source file paths.
*/
public Map<String, String> getSourceFileMap() {
public synchronized Map<String, String> getSourceFileMap() {
if (sourceFileMap == null) {
sourceFileMap = Files.exists(zipfs.getPath("/webinspect.xml"))
? new ConcurrentHashMap<>()
: loadSourceFileMap();
}
return sourceFileMap;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,47 +82,61 @@ public static int fuzzySearchContext(List<String> sourceLines, List<String> cont
return -1; // Not found
}

public static int[] fuzzySearchOriginalCode(List<String> sourceLines, List<String> originalCodeLine, int maxMismatches, int startIndex){
public static int[] fuzzySearchOriginalCode(List<String> sourceLines, List<String> originalCodeLine, int maxMismatches, int startIndex) {
List<String> normalizedSource = normalizeLines(sourceLines);
List<String> normalizedOriginalCode = normalizeLines(originalCodeLine);
int[] lineFromTo = new int[2]; //0th index represents original line starting line number in sourceLine
//1st index represents original line end line number in sourceLine
int misMatches;
int sourceIndex;
for(int i=startIndex; i<normalizedSource.size(); i++)
{
misMatches=0;
sourceIndex = i;
if(normalizedSource.get(i).isEmpty())

for (int i = Math.max(0, startIndex); i < normalizedSource.size(); i++) {
if (normalizedSource.get(i).isEmpty()) {
continue;
for(int j=0; j<normalizedOriginalCode.size(); j++){
if(normalizedOriginalCode.get(j).isEmpty())
continue;
if( j>0 && normalizedSource.get(sourceIndex).isEmpty())
{
sourceIndex++;
//continue;
}
if(!linesSimilar(normalizedSource.get(sourceIndex), normalizedOriginalCode.get(j)))
{
misMatches++;
if(misMatches>maxMismatches)
break;
}
if(j==normalizedOriginalCode.size()-1)
{
lineFromTo[0] = i;
lineFromTo[1] = sourceIndex;
return lineFromTo;
}

int lineTo = findOriginalCodeEnd(normalizedSource, normalizedOriginalCode, maxMismatches, i);
if (lineTo != -1) {
return new int[] {i, lineTo};
}
}

return new int[] {-1, -1};
}

private static int findOriginalCodeEnd(List<String> normalizedSource, List<String> normalizedOriginalCode, int maxMismatches,
int sourceIndex) {
int mismatches = 0;
int lineTo = -1;
boolean matchedAnyLine = false;

for (String originalCodeLine : normalizedOriginalCode) {
if (originalCodeLine.isEmpty()) {
continue;
}

if (matchedAnyLine) {
sourceIndex = skipEmptySourceLines(normalizedSource, sourceIndex);
}
if (sourceIndex >= normalizedSource.size()) {
return -1;
}

if (!linesSimilar(normalizedSource.get(sourceIndex), originalCodeLine)) {
mismatches++;
if (mismatches > maxMismatches) {
return -1;
}
sourceIndex++;
}

lineTo = sourceIndex++;
matchedAnyLine = true;
}

return matchedAnyLine ? lineTo : -1;
}

private static int skipEmptySourceLines(List<String> normalizedSource, int sourceIndex) {
while (sourceIndex < normalizedSource.size() && normalizedSource.get(sourceIndex).isEmpty()) {
sourceIndex++;
}
//Original Code lines not found in source Code
lineFromTo[0] = -1;
lineFromTo[1] = -1;
return lineFromTo;
return sourceIndex;
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* 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.aviator.util;

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

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.xml.sax.SAXException;

import com.fortify.cli.aviator._common.exception.AviatorTechnicalException;

@DisplayName("FprHandle")
class FprHandleTest {

@TempDir
Path tempDir;

@Test
@DisplayName("parses simple index XML without DOCTYPE")
void parsesSimpleIndexXmlWithoutDoctype() throws Exception {
Path fprPath = createFpr("""
<?xml version="1.0" encoding="UTF-8"?>
<index>
<entry key="Test.java">src-archive/Test.java</entry>
</index>
""");

try (FprHandle handle = new FprHandle(fprPath)) {
assertEquals("src-archive/Test.java", handle.getSourceFileMap().get("Test.java"));
}
}

@Test
@DisplayName("defers malformed source index parsing until source map is requested")
void defersMalformedSourceIndexParsingUntilSourceMapIsRequested() throws Exception {
Path fprPath = createFpr("""
<?xml version="1.0" encoding="UTF-8"?>
<index>
<entry key="Test.java">src-archive/Test.java</index>
""");

try (FprHandle handle = new FprHandle(fprPath)) {
AviatorTechnicalException exception = assertThrows(AviatorTechnicalException.class, handle::getSourceFileMap);

assertTrue(exception.getCause() instanceof SAXException);
}
}

@Test
@DisplayName("validate parses source map after source presence checks")
void validateParsesSourceMapAfterSourcePresenceChecks() throws Exception {
Path fprPath = createFpr("""
<?xml version="1.0" encoding="UTF-8"?>
<index>
<entry key="Test.java">src-archive/Test.java</index>
""");

try (FprHandle handle = new FprHandle(fprPath)) {
AviatorTechnicalException exception = assertThrows(AviatorTechnicalException.class, handle::validate);

assertTrue(exception.getCause() instanceof SAXException);
}
}

@Test
@DisplayName("opens remediation-only FPR without requiring source archive index")
void opensRemediationOnlyFprWithoutRequiringSourceArchiveIndex() throws Exception {
Path fprPath = createFprWithoutSourceIndex();

try (FprHandle handle = new FprHandle(fprPath)) {
assertTrue(handle.hasRemediations());
}
}

private Path createFpr(String indexXml) throws IOException {
Path fprPath = tempDir.resolve("test.fpr");
try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(fprPath))) {
writeEntry(zipOutputStream, "audit.fvdl", "<FVDL />");
writeEntry(zipOutputStream, "src-archive/index.xml", indexXml);
writeEntry(zipOutputStream, "src-archive/Test.java", "public class Test {}\n");
}
return fprPath;
}

private Path createFprWithoutSourceIndex() throws IOException {
Path fprPath = tempDir.resolve("remediation-only.fpr");
try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(fprPath))) {
writeEntry(zipOutputStream, "remediations.xml", "<Remediations />");
}
return fprPath;
}

private void writeEntry(ZipOutputStream zipOutputStream, String entryName, String content) throws IOException {
zipOutputStream.putNextEntry(new ZipEntry(entryName));
zipOutputStream.write(content.getBytes(StandardCharsets.UTF_8));
zipOutputStream.closeEntry();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.aviator.util;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;

import java.util.List;

import org.junit.jupiter.api.Test;

class FuzzyContextSearcherTest {

@Test
void shouldReturnNotFoundWhenOriginalCodeRunsPastEndOfSource() {
int[] lineFromTo = FuzzyContextSearcher.fuzzySearchOriginalCode(
List.of("line one", "line two"),
List.of("line two", "line three"),
0,
1);

assertArrayEquals(new int[] {-1, -1}, lineFromTo);
}

@Test
void shouldMatchOriginalCodeAcrossBlankSourceLines() {
int[] lineFromTo = FuzzyContextSearcher.fuzzySearchOriginalCode(
List.of("line one", "", "", "line two"),
List.of("line one", "line two"),
0,
0);

assertArrayEquals(new int[] {0, 3}, lineFromTo);
}
}
Loading