1 package emissary.kff;
2
3 import emissary.core.channels.FileChannelFactory;
4 import emissary.core.channels.SeekableByteChannelFactory;
5 import emissary.test.core.junit5.UnitTest;
6 import emissary.util.io.ResourceReader;
7
8 import org.apache.commons.compress.utils.ByteUtils;
9 import org.apache.commons.lang3.ArrayUtils;
10 import org.apache.commons.lang3.Validate;
11 import org.junit.jupiter.api.BeforeEach;
12 import org.junit.jupiter.api.Test;
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15
16 import java.io.IOException;
17 import java.nio.ByteBuffer;
18 import java.nio.channels.SeekableByteChannel;
19 import java.nio.file.Path;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Random;
25 import java.util.concurrent.Callable;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ExecutorService;
28 import java.util.concurrent.Executors;
29 import java.util.concurrent.Future;
30 import java.util.stream.Collectors;
31
32 import static emissary.kff.KffFile.DEFAULT_RECORD_LENGTH;
33 import static org.junit.jupiter.api.Assertions.assertEquals;
34 import static org.junit.jupiter.api.Assertions.assertFalse;
35 import static org.junit.jupiter.api.Assertions.assertTrue;
36 import static org.junit.jupiter.api.Assertions.fail;
37
38 class KffFileTest extends UnitTest {
39 public static final Random RANDOM = new Random();
40 private static final Logger LOGGER = LoggerFactory.getLogger(KffFileTest.class);
41
42 private static final String ITEM_NAME = "Some_item_name";
43 private static final byte[] expectedSha1Bytes = {(byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) 103, (byte) 56, (byte) 116,
44 (byte) -114, (byte) -35, (byte) -110, (byte) -60, (byte) -29, (byte) -46, (byte) -24, (byte) 35, (byte) -119,
45 (byte) 103, (byte) 0, (byte) -8, (byte) 73};
46 private static final byte[] expectedCrcBytes = {(byte) -21, (byte) -47, (byte) 5, (byte) -96};
47 @SuppressWarnings("NonFinalStaticField")
48 private static KffFile kffFile;
49 private static final String resourcePath = new ResourceReader()
50 .getResource("emissary/kff/KffFileTest/tmp.bin").getPath();
51
52 SeekableByteChannelFactory channelFactory = FileChannelFactory.create(Path.of(resourcePath));
53
54 @Override
55 @BeforeEach
56 public void setUp() throws Exception {
57 kffFile = new KffFile(resourcePath, "testFilter", KffFilter.FilterType.UNKNOWN);
58 kffFile.setPreferredAlgorithm("SHA-1");
59 }
60
61 @Test
62 void testKffFileCreation() {
63 assertEquals("testFilter", kffFile.getName());
64 kffFile.setFilterType(KffFilter.FilterType.IGNORE);
65 assertEquals(KffFilter.FilterType.IGNORE, kffFile.getFilterType());
66 assertEquals("SHA-1", kffFile.getPreferredAlgorithm());
67 }
68
69 @Test
70 void testKffFileCheck() {
71 ChecksumResults results = new ChecksumResults();
72 results.setHash("SHA-1", expectedSha1Bytes);
73 results.setHash("CRC32", expectedCrcBytes);
74 try {
75 assertTrue(kffFile.check(ITEM_NAME, results));
76 } catch (Exception e) {
77 fail(e);
78 }
79 byte[] incorrectSha1Bytes = {(byte) 0, (byte) 0, (byte) 0, (byte) 32, (byte) 103, (byte) 56, (byte) 116,
80 (byte) -114, (byte) -35, (byte) -110, (byte) -60, (byte) -29, (byte) -46, (byte) -24, (byte) 35, (byte) -119,
81 (byte) 103, (byte) 0, (byte) -8, (byte) 70};
82 results = new ChecksumResults();
83 results.setHash("SHA-1", incorrectSha1Bytes);
84 try {
85 assertFalse(kffFile.check(ITEM_NAME, results));
86 } catch (Exception e) {
87 fail(e);
88 }
89 }
90
91
92
93
94 @Test
95 void testConcurrentKffFileCheckCalls() throws ExecutionException, IOException, InterruptedException {
96 int EXPECTED_FAILURE_COUNT = 200;
97
98
99 List<CheckTestInput> testInputs = new ArrayList<>();
100
101
102 parseRecordsFromBinaryFileAndAddToTestInputs(testInputs);
103 int numberOfKffEntriesInTestFile = testInputs.size();
104
105
106 createRecordsFromRandomBytesAndAddToTestInputs(testInputs, EXPECTED_FAILURE_COUNT);
107
108 shuffleTestInputs(testInputs);
109
110 List<KffFileCheckTask> callables = createCallableTasksForParallelExecution(testInputs);
111
112 logger.debug("testing {} invocations, with {} that should return true", callables.size(), numberOfKffEntriesInTestFile);
113
114 ExecutorService executorService = null;
115 try {
116 executorService = Executors.newFixedThreadPool(10);
117
118 List<Future<Boolean>> results = executorService.invokeAll(callables);
119 for (Future<Boolean> result : results) {
120 assertTrue(result.get(), "kffFile.check result didn't match expectations");
121 }
122 } finally {
123 if (executorService != null) {
124 executorService.shutdown();
125 }
126 }
127 }
128
129 private static void createRecordsFromRandomBytesAndAddToTestInputs(List<CheckTestInput> testInputs, int recordCount) {
130 for (int i = 0; i < recordCount; i++) {
131
132
133 ChecksumResults csr = buildCheckSumResultsFromRandomBytes();
134 CheckTestInput expectedFailure = new CheckTestInput(csr, false);
135 testInputs.add(expectedFailure);
136 }
137 }
138
139 private void parseRecordsFromBinaryFileAndAddToTestInputs(List<CheckTestInput> testInputs) throws IOException {
140 int numberOfKffEntriesInTestFile;
141
142 try (SeekableByteChannel byteChannel = channelFactory.create()) {
143 numberOfKffEntriesInTestFile = (int) (byteChannel.size() / DEFAULT_RECORD_LENGTH);
144 LOGGER.debug("test file contains {} known file entries", numberOfKffEntriesInTestFile);
145
146 for (int i = 0; i < numberOfKffEntriesInTestFile; i++) {
147 ChecksumResults csr = buildCheckSumResultsFromKffFileBytes(byteChannel, i * DEFAULT_RECORD_LENGTH);
148 CheckTestInput expectedSuccess = new CheckTestInput(csr, true);
149 testInputs.add(expectedSuccess);
150 }
151 }
152 }
153
154
155
156
157
158
159 private static void shuffleTestInputs(List<CheckTestInput> testInputs) {
160 Collections.shuffle(testInputs);
161 }
162
163
164
165
166
167
168
169
170
171 private static ChecksumResults buildCheckSumResultsFromKffFileBytes(SeekableByteChannel sbc, int startPosition) throws IOException {
172 sbc.position(startPosition);
173 ByteBuffer buffer = ByteBuffer.wrap(new byte[DEFAULT_RECORD_LENGTH]);
174
175 sbc.read(buffer);
176
177 return buildChecksumResultsWithSha1AndCrc(buffer.array());
178 }
179
180
181
182
183
184
185 private static ChecksumResults buildCheckSumResultsFromRandomBytes() {
186 byte[] randomBytes = new byte[DEFAULT_RECORD_LENGTH];
187 RANDOM.nextBytes(randomBytes);
188
189 return buildChecksumResultsWithSha1AndCrc(randomBytes);
190 }
191
192
193
194
195
196
197
198 private static ChecksumResults buildChecksumResultsWithSha1AndCrc(byte[] recordBytes) {
199 Validate.notNull(recordBytes, "recordBytes must not be null");
200 Validate.isTrue(recordBytes.length == DEFAULT_RECORD_LENGTH, "recordBytes must include 24 elements");
201 byte[] sha1Bytes = getSha1Bytes(recordBytes);
202 byte[] crc32Bytes = getCrc32BytesLe(recordBytes);
203 ChecksumResults csr = new ChecksumResults();
204 csr.setHash("SHA-1", sha1Bytes);
205 csr.setCrc(ByteUtils.fromLittleEndian(crc32Bytes));
206 return csr;
207 }
208
209
210
211
212
213
214
215 private static byte[] getSha1Bytes(byte[] recordBytes) {
216 Validate.notNull(recordBytes, "recordBytes must not be null");
217 Validate.isTrue(recordBytes.length == DEFAULT_RECORD_LENGTH, "recordBytes must include 24 elements");
218 return Arrays.copyOfRange(recordBytes, 0, DEFAULT_RECORD_LENGTH - 4);
219 }
220
221
222
223
224
225
226
227 private static byte[] getCrc32BytesLe(byte[] recordBytes) {
228 Validate.notNull(recordBytes, "recordBytes must not be null");
229 Validate.isTrue(recordBytes.length == DEFAULT_RECORD_LENGTH, "recordBytes must include 24 elements");
230 byte[] result = Arrays.copyOfRange(recordBytes, DEFAULT_RECORD_LENGTH - 4, DEFAULT_RECORD_LENGTH);
231 ArrayUtils.reverse(result);
232 return result;
233 }
234
235
236
237
238
239
240
241 private static List<KffFileCheckTask> createCallableTasksForParallelExecution(List<CheckTestInput> testInputs) {
242 return testInputs.stream().map(input -> new KffFileCheckTask(kffFile, input.csr, input.expectedResult))
243 .collect(Collectors.toList());
244 }
245
246
247
248
249 static class KffFileCheckTask implements Callable<Boolean> {
250 private final KffFile kffFile;
251 private final ChecksumResults csr;
252 private final Boolean expectedResult;
253
254 KffFileCheckTask(KffFile kffFile, ChecksumResults csr, boolean expectedResult) {
255 this.kffFile = kffFile;
256 this.csr = csr;
257 this.expectedResult = expectedResult;
258 }
259
260
261
262
263
264
265
266 @Override
267 public Boolean call() throws Exception {
268 boolean actual = kffFile.check("ignored param", csr);
269
270 LOGGER.debug("expected {}, got {}", expectedResult, actual);
271 return expectedResult.equals(actual);
272 }
273 }
274
275
276
277
278
279 static class CheckTestInput {
280 final ChecksumResults csr;
281 final boolean expectedResult;
282
283 CheckTestInput(ChecksumResults csr, boolean expectedResult) {
284 this.csr = csr;
285 this.expectedResult = expectedResult;
286 }
287 }
288 }