Skip to content

Commit b3a041d

Browse files
committed
chore(SP-2487): add test case to verify correct obfuscation
1 parent f9928a5 commit b3a041d

3 files changed

Lines changed: 282 additions & 12 deletions

File tree

src/main/java/com/scanoss/Winnowing.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import java.nio.charset.Charset;
4141
import java.nio.file.Files;
4242
import java.util.*;
43+
import java.util.concurrent.ConcurrentHashMap;
44+
import java.util.concurrent.atomic.AtomicLong;
4345
import java.util.zip.CRC32C;
4446
import java.util.zip.Checksum;
4547

@@ -71,10 +73,13 @@ public class Winnowing {
7173
@Builder.Default
7274
private int snippetLimit = MAX_LONG_LINE_CHARS; // Enable limiting of size of a single line of snippet generation
7375
@Builder.Default
74-
private Map<String, String> obfuscationMap = new HashMap<>();
76+
private Map<String, String> obfuscationMap = new ConcurrentHashMap<>();
77+
@Builder.Default
78+
private static final AtomicLong idGenerator = new AtomicLong(0); //Incremental ids used for obfuscating path
7579

7680
/**
7781
* Resolves the real file path for a given obfuscated path.
82+
* This method is thread-safe and can be called concurrently from multiple threads.
7883
*
7984
* @param obfuscatedPath the obfuscated path
8085
* @return the real file path corresponding to the provided obfuscated path, or null if no mapping exists
@@ -200,19 +205,17 @@ public String wfpForContents(@NonNull String filename, Boolean binFile, byte[] c
200205

201206
/**
202207
* Obfuscates the given file path by replacing it with a generated unique identifier while
203-
* retaining its original file extension. The obfuscated path can be used to mask
204-
* sensitive or easily guessable file names.
208+
* retaining its original file extension.
209+
* This method is thread-safe and can be called concurrently from multiple threads.
205210
*
206211
* @param originalPath the original file path to be obfuscated; must not be null
207212
* @return the obfuscated file path with a unique identifier and the original file extension
208213
*/
209214
private String obfuscateFilePath(@NotNull String originalPath) {
210215
final String extension = extractExtension(originalPath);
211216

212-
// Generate a unique identifier for the obfuscated file
213-
final int mapIndex = obfuscationMap.size();
214-
215-
final String obfuscatedPath = mapIndex + extension;
217+
// Generate a unique identifier for the obfuscated file using a thread-safe approach
218+
final String obfuscatedPath = idGenerator.getAndIncrement() + extension;
216219
this.obfuscationMap.put(obfuscatedPath, originalPath);
217220
return obfuscatedPath;
218221
}

src/test/java/com/scanoss/TestScanner.java

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,61 @@
2222
*/
2323
package com.scanoss;
2424

25+
import com.google.gson.Gson;
26+
import com.scanoss.dto.ScanFileDetails;
27+
import com.scanoss.dto.ScanFileResult;
28+
import com.scanoss.dto.ServerDetails;
29+
import com.scanoss.dto.enums.MatchType;
2530
import com.scanoss.exceptions.ScannerException;
2631
import com.scanoss.filters.FilterConfig;
2732
import com.scanoss.settings.ScanossSettings;
2833
import com.scanoss.utils.JsonUtils;
34+
import com.scanoss.utils.WinnowingUtils;
2935
import lombok.extern.slf4j.Slf4j;
36+
import okhttp3.mockwebserver.Dispatcher;
37+
import okhttp3.mockwebserver.MockResponse;
38+
import okhttp3.mockwebserver.MockWebServer;
39+
import okhttp3.mockwebserver.RecordedRequest;
40+
import org.jetbrains.annotations.NotNull;
41+
import org.junit.After;
3042
import org.junit.Before;
3143
import org.junit.Test;
3244

45+
import java.io.File;
3346
import java.io.FileWriter;
3447
import java.io.IOException;
35-
import java.util.ArrayList;
36-
import java.util.Arrays;
37-
import java.util.List;
48+
import java.nio.file.*;
49+
import java.nio.file.attribute.BasicFileAttributes;
50+
import java.util.*;
51+
import java.util.concurrent.*;
52+
import java.util.stream.Collectors;
3853

3954
import static org.junit.Assert.*;
4055

4156
@Slf4j
4257
public class TestScanner {
58+
private MockWebServer server;
59+
60+
4361
@Before
44-
public void Setup() {
62+
public void Setup() throws IOException{
4563
log.info("Starting Scanner test cases...");
4664
log.debug("Logging debug enabled");
4765
log.trace("Logging trace enabled");
66+
log.info("Starting Mock Server...");
67+
server = new MockWebServer();
68+
server.start();
69+
}
70+
71+
@After
72+
public void Finish() {
73+
log.info("Shutting down mock server.");
74+
try {
75+
server.close();
76+
server.shutdown();
77+
} catch (IOException e) {
78+
log.warn("Some issue shutting down mock server: {}", e.getLocalizedMessage());
79+
}
4880
}
4981

5082
@Test
@@ -409,4 +441,116 @@ public void TestScannerCustomFilterConfig() {
409441

410442
log.info("Finished {} -->", methodName);
411443
}
412-
}
444+
445+
/**
446+
* Test that we can scan a folder with obfuscation enabled using a mock server.
447+
* This test focuses on the path obfuscation/deobfuscation functionality in a multi-threaded environment.
448+
* The dispatcher supports handling multiple files in a single request.
449+
*/
450+
@Test
451+
public void testConcurrentScanWithObfuscation() throws IOException {
452+
final String folderToScan = "src/test";
453+
454+
// Set to capture all paths received by the server
455+
final Set<String> receivedPaths = ConcurrentHashMap.newKeySet();
456+
457+
// Collect all files in the src/test folder before scanning
458+
final Set<String> allSourceFilePaths = Files.walk(Paths.get(folderToScan))
459+
.filter(path -> !Files.isDirectory(path))
460+
.map(path -> {
461+
// Convert to relative path with forward slashes
462+
String relativePath = Paths.get(folderToScan).relativize(path).toString();
463+
return relativePath.replace(File.separatorChar, '/');
464+
})
465+
.collect(Collectors.toSet());
466+
467+
log.info("Found {} files in source directory", allSourceFilePaths.size());
468+
469+
470+
471+
472+
473+
// Configure the MockWebServer to return a 'no match' response for any request.
474+
// This is important for testing without depending on actual scan results.
475+
//TODO: Extend the mock webserver to other tests.
476+
final Dispatcher dispatcher = new Dispatcher() {
477+
@NotNull
478+
@Override
479+
public MockResponse dispatch(RecordedRequest request) {
480+
// Extract the WFP from the request and parse all obfuscated paths
481+
String requestBody = request.getBody().readUtf8();
482+
Set<String> paths = WinnowingUtils.extractFilePathsFromWFPBlock(requestBody);
483+
484+
// Store all received paths for later verification
485+
receivedPaths.addAll(paths);
486+
487+
488+
for (String path : paths) {
489+
log.debug("Server received obfuscated path: {}", path);
490+
}
491+
492+
if (paths.isEmpty()) {
493+
return new MockResponse()
494+
.setResponseCode(400)
495+
.setBody("error: Bad Request - No valid obfuscated paths found");
496+
}
497+
498+
// Create response objects using the DTO classes
499+
Map<String, List<ScanFileDetails>> responseMap = new HashMap<>();
500+
501+
// Create server details object (same for all responses)
502+
ServerDetails.KbVersion kbVersion = new ServerDetails.KbVersion("25.05", "21.05.21");
503+
ServerDetails serverDetails = new ServerDetails("5.4.10", kbVersion);
504+
505+
// Create a "none" match result for each path
506+
for (String path : paths) {
507+
ScanFileDetails noMatchResult = ScanFileDetails.builder()
508+
.matchType(MatchType.none)
509+
.serverDetails(serverDetails)
510+
.build();
511+
512+
responseMap.put(path, Collections.singletonList(noMatchResult));
513+
}
514+
515+
// Convert to JSON
516+
Gson gson = new Gson();
517+
String responseJson = gson.toJson(responseMap);
518+
519+
return new MockResponse()
520+
.setResponseCode(200)
521+
.setBody(responseJson);
522+
}
523+
};
524+
server.setDispatcher(dispatcher);
525+
526+
// Create a scanner with obfuscation enabled and multiple threads
527+
Scanner scanner = Scanner.builder()
528+
.obfuscate(true)
529+
.numThreads(8) // Use multiple threads to process files
530+
.url(server.url("/api/scan/direct").toString()) // Use our mock server
531+
.build();
532+
533+
// Scan the files to test the full obfuscation/deobfuscation cycle
534+
List<String> results = scanner.scanFolder(folderToScan);
535+
536+
537+
// Verify we got scan results
538+
assertNotNull("Should have scan results", results);
539+
assertFalse("Should have result non empty", results.isEmpty());
540+
log.info("Received {} scan results", results.size());
541+
542+
// Verify paths received by the server are obfuscated (not matching any source file paths)
543+
for (String receivedPath : receivedPaths) {
544+
assertFalse("Path should be obfuscated and not match any source path: " + receivedPath,
545+
allSourceFilePaths.contains(receivedPath));
546+
}
547+
548+
List<ScanFileResult> resultsDto = JsonUtils.toScanFileResults(results);
549+
// Verify (deobfuscation) that all results from scanFolder are valid file paths from our source directory
550+
for (ScanFileResult r : resultsDto) {
551+
assertTrue("Result should be a valid source file path: " + r.getFilePath(),
552+
allSourceFilePaths.contains(r.getFilePath()));
553+
}
554+
555+
}
556+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// SPDX-License-Identifier: MIT
2+
/*
3+
* Copyright (c) 2025, SCANOSS
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in
13+
* all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
* THE SOFTWARE.
22+
*/
23+
package com.scanoss;
24+
25+
import com.scanoss.exceptions.WinnowingException;
26+
import com.scanoss.utils.WinnowingUtils;
27+
import lombok.extern.slf4j.Slf4j;
28+
import org.junit.Test;
29+
30+
import java.io.File;
31+
import java.nio.file.Path;
32+
import java.util.*;
33+
import java.util.concurrent.*;
34+
import java.util.stream.Collectors;
35+
36+
import static org.junit.Assert.*;
37+
38+
/**
39+
* Tests to validate thread safety of the path obfuscation feature in the Winnowing class.
40+
*/
41+
@Slf4j
42+
public class WinnowingConcurrencyTest {
43+
44+
/**
45+
* Test that concurrent obfuscation of paths works correctly without data loss or corruption.
46+
* This simulates multiple threads processing different files simultaneously.
47+
*/
48+
@Test
49+
public void testConcurrentObfuscation() throws InterruptedException, ExecutionException {
50+
// Create a Winnowing instance with obfuscation enabled
51+
Winnowing winnowing = Winnowing.builder().obfuscate(true).build();
52+
53+
// Number of concurrent threads to use
54+
int threadCount = 50;
55+
56+
// Create a thread pool
57+
ExecutorService executor = Executors.newFixedThreadPool(10);
58+
59+
// Create a list of paths to obfuscate
60+
List<String> paths = new ArrayList<>();
61+
for (int i = 0; i < threadCount; i++) {
62+
paths.add("/path/to/file" + i + ".java");
63+
}
64+
65+
// Tasks for concurrent obfuscation
66+
List<Future<String>> obfuscationTasks = new ArrayList<>();
67+
68+
// Submit tasks to executor - each task will obfuscate a path and return the WFP
69+
for (String path : paths) {
70+
obfuscationTasks.add(executor.submit(() -> {
71+
byte[] contents = ("sample content for " + path).getBytes();
72+
return winnowing.wfpForContents(path, false, contents);
73+
}));
74+
}
75+
76+
// Collect results
77+
List<String> results = new ArrayList<>();
78+
for (Future<String> future : obfuscationTasks) {
79+
results.add(future.get());
80+
}
81+
82+
// Shutdown executor
83+
executor.shutdown();
84+
assertTrue("Executor should terminate within timeout", executor.awaitTermination(10, TimeUnit.SECONDS));
85+
86+
// Verify each path was processed successfully
87+
assertEquals("All paths should have been processed", threadCount, results.size());
88+
89+
// Extract the obfuscated paths from the WFPs
90+
Set<String> obfuscatedPaths = new HashSet<>();
91+
Map<String, String> originalToObfuscated = new HashMap<>();
92+
93+
for (int i = 0; i < results.size(); i++) {
94+
String wfp = results.get(i);
95+
String obfuscatedPath = WinnowingUtils.extractFilePathFromWFP(wfp);
96+
97+
assertNotNull("Obfuscated path should not be null", obfuscatedPath);
98+
assertTrue("Obfuscated path should end with .java", obfuscatedPath.endsWith(".java"));
99+
100+
// Store for uniqueness check
101+
obfuscatedPaths.add(obfuscatedPath);
102+
103+
// Verify deobfuscation works
104+
String originalPath = winnowing.deobfuscateFilePath(obfuscatedPath);
105+
assertNotNull("Original path should be recoverable", originalPath);
106+
assertTrue("Original path should match one of the input paths", paths.contains(originalPath));
107+
108+
// Map original to obfuscated for additional checking
109+
originalToObfuscated.put(paths.get(i), obfuscatedPath);
110+
}
111+
112+
// Verify all obfuscated paths are unique
113+
assertEquals("Each path should have a unique obfuscated value", threadCount, obfuscatedPaths.size());
114+
115+
// Verify all original paths can be deobfuscated
116+
for (String originalPath : paths) {
117+
String obfuscatedPath = originalToObfuscated.get(originalPath);
118+
String recoveredPath = winnowing.deobfuscateFilePath(obfuscatedPath);
119+
assertEquals("Deobfuscation should recover the original path", originalPath, recoveredPath);
120+
}
121+
}
122+
123+
}

0 commit comments

Comments
 (0)