|
20 | 20 | import static java.nio.charset.StandardCharsets.UTF_16BE; |
21 | 21 | import static java.nio.charset.StandardCharsets.UTF_16LE; |
22 | 22 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 23 | +import static org.junit.jupiter.api.Assertions.assertFalse; |
| 24 | +import static org.junit.jupiter.api.Assertions.assertTrue; |
23 | 25 | import static org.junit.jupiter.api.Assertions.fail; |
24 | 26 |
|
25 | 27 | import java.io.ByteArrayInputStream; |
26 | 28 | import java.io.IOException; |
27 | 29 | import java.io.InputStream; |
| 30 | +import java.util.ArrayList; |
| 31 | +import java.util.List; |
| 32 | +import java.util.concurrent.ExecutorService; |
| 33 | +import java.util.concurrent.Executors; |
| 34 | +import java.util.concurrent.Future; |
| 35 | +import java.util.concurrent.TimeUnit; |
28 | 36 |
|
29 | 37 | import org.apache.commons.io.IOUtils; |
30 | 38 | import org.junit.jupiter.api.Test; |
@@ -207,6 +215,100 @@ public void testDetectString() throws Exception { |
207 | 215 | assertDetect(detector, testMT, data.getBytes(US_ASCII)); |
208 | 216 | } |
209 | 217 |
|
| 218 | + /** |
| 219 | + * The byte[] path is what MagicMatch.eval uses for every magic in |
| 220 | + * tika-mimetypes.xml, but it was only ever exercised indirectly. Cover the |
| 221 | + * regex branch of it directly. |
| 222 | + */ |
| 223 | + @Test |
| 224 | + public void testMatchesByteArrayRegEx() { |
| 225 | + MediaType pdf = new MediaType("application", "pdf"); |
| 226 | + MagicDetector detector = |
| 227 | + new MagicDetector(pdf, "(?s)\\A.{0,144}%PDF-".getBytes(US_ASCII), null, true, 0, 0); |
| 228 | + |
| 229 | + assertTrue(detector.matches("%PDF-1.0".getBytes(US_ASCII))); |
| 230 | + assertTrue(detector.matches(("0 10 20 30 40 50 6" + |
| 231 | + "0 70 80 90 100 110 1" + |
| 232 | + "20 130 140" + "34%PDF-1.0").getBytes(US_ASCII))); |
| 233 | + assertFalse(detector.matches(("0 10 20 30 40 50 6" + |
| 234 | + "0 70 80 90 100 110 1" + |
| 235 | + "20 130 140" + "345%PDF-1.0").getBytes(US_ASCII))); |
| 236 | + assertFalse(detector.matches("".getBytes(US_ASCII))); |
| 237 | + assertFalse(detector.matches(null)); |
| 238 | + |
| 239 | + // an offset range, mirroring the wider windows used in tika-mimetypes.xml |
| 240 | + MediaType xhtml = new MediaType("application", "xhtml+xml"); |
| 241 | + String pattern = "(?s)\\x3chtml xmlns=\"http://www\\.w3\\.org/1999/xhtml" + |
| 242 | + "\".*\\x3ctitle\\x3e.*\\x3c/title\\x3e"; |
| 243 | + MagicDetector ranged = |
| 244 | + new MagicDetector(xhtml, pattern.getBytes(US_ASCII), null, true, 0, 8192); |
| 245 | + assertTrue(ranged.matches(("<html xmlns=\"http://www.w3.org/1999/xhtml\">" + |
| 246 | + "<head><title>XHTML test document</title></head>").getBytes(US_ASCII))); |
| 247 | + assertFalse(ranged.matches("<html><head><title>no namespace</title></head>" |
| 248 | + .getBytes(US_ASCII))); |
| 249 | + } |
| 250 | + |
| 251 | + /** |
| 252 | + * A MagicDetector is built once and reused for the life of the process, so |
| 253 | + * repeated calls must be independent of each other. Guards the compiled |
| 254 | + * Pattern against per-call state leaking in. |
| 255 | + */ |
| 256 | + @Test |
| 257 | + public void testRegExDetectorRepeatedCallsStable() throws Exception { |
| 258 | + MediaType html = new MediaType("text", "html"); |
| 259 | + String pattern = "(?s)\\A.{0,1024}\\x3c\\!(?:DOCTYPE|doctype) (?:HTML|html) "; |
| 260 | + MagicDetector detector = |
| 261 | + new MagicDetector(html, pattern.getBytes(US_ASCII), null, true, 0, 0); |
| 262 | + |
| 263 | + byte[] match = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">".getBytes(US_ASCII); |
| 264 | + byte[] noMatch = "<html><head><title>plain</title></head>".getBytes(US_ASCII); |
| 265 | + |
| 266 | + for (int i = 0; i < 100; i++) { |
| 267 | + assertTrue(detector.matches(match), "matches() changed on iteration " + i); |
| 268 | + assertFalse(detector.matches(noMatch), "matches() changed on iteration " + i); |
| 269 | + assertDetect(detector, html, match); |
| 270 | + assertDetect(detector, MediaType.OCTET_STREAM, noMatch); |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + /** |
| 275 | + * MimeTypes shares one MagicDetector instance per magic clause across every |
| 276 | + * caller, so the regex path has to be safe to use concurrently. |
| 277 | + */ |
| 278 | + @Test |
| 279 | + public void testRegExDetectorConcurrent() throws Exception { |
| 280 | + MediaType pdf = new MediaType("application", "pdf"); |
| 281 | + MagicDetector detector = |
| 282 | + new MagicDetector(pdf, "(?s)\\A.{0,144}%PDF-".getBytes(US_ASCII), null, true, 0, 0); |
| 283 | + |
| 284 | + byte[] match = "%PDF-1.4\nsome trailing content".getBytes(US_ASCII); |
| 285 | + byte[] noMatch = "not a pdf at all".getBytes(US_ASCII); |
| 286 | + |
| 287 | + int threads = 8; |
| 288 | + int iterations = 200; |
| 289 | + ExecutorService executor = Executors.newFixedThreadPool(threads); |
| 290 | + try { |
| 291 | + List<Future<?>> futures = new ArrayList<>(); |
| 292 | + for (int t = 0; t < threads; t++) { |
| 293 | + futures.add(executor.submit(() -> { |
| 294 | + for (int i = 0; i < iterations; i++) { |
| 295 | + assertTrue(detector.matches(match)); |
| 296 | + assertFalse(detector.matches(noMatch)); |
| 297 | + assertEquals(pdf, detector.detect(TikaInputStream.get(match), new Metadata(), |
| 298 | + new ParseContext())); |
| 299 | + } |
| 300 | + return null; |
| 301 | + })); |
| 302 | + } |
| 303 | + for (Future<?> future : futures) { |
| 304 | + // an assertion failure on a worker surfaces here as an ExecutionException |
| 305 | + future.get(60, TimeUnit.SECONDS); |
| 306 | + } |
| 307 | + } finally { |
| 308 | + executor.shutdownNow(); |
| 309 | + } |
| 310 | + } |
| 311 | + |
210 | 312 | private void assertDetect(Detector detector, MediaType type, String data) { |
211 | 313 | byte[] bytes = data.getBytes(US_ASCII); |
212 | 314 | assertDetect(detector, type, bytes); |
|
0 commit comments