diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e178ae4..8717dd14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Added +- Some more binary file types detection, by @HardNorth +### Changed +- `jackson-databind` dependency reverted to `api` type, by @HardNorth ## [5.2.7] ### Changed diff --git a/build.gradle b/build.gradle index 01e97e68..bb9dd83e 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ dependencies { api ("com.squareup.retrofit2:retrofit:${project.retrofit_version}") { exclude module: 'okhttp' } - implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.7.1' + api 'com.fasterxml.jackson.core:jackson-databind:2.12.7.1' // Access is needed by HTTP loggers to format JSON implementation "com.squareup.retrofit2:converter-scalars:${project.retrofit_version}" implementation ("com.squareup.retrofit2:converter-jackson:${project.retrofit_version}") { exclude module: 'jackson-databind' diff --git a/src/main/java/com/epam/reportportal/utils/MimeTypeDetector.java b/src/main/java/com/epam/reportportal/utils/MimeTypeDetector.java index e4e1a13f..839388ce 100644 --- a/src/main/java/com/epam/reportportal/utils/MimeTypeDetector.java +++ b/src/main/java/com/epam/reportportal/utils/MimeTypeDetector.java @@ -37,7 +37,7 @@ public class MimeTypeDetector { private static final String UNKNOWN_TYPE = "application/octet-stream"; private static final String EXTENSION_DELIMITER = "."; - private static final int BYTES_TO_READ_FOR_DETECTION = 20; + private static final int BYTES_TO_READ_FOR_DETECTION = 128; private static final Map ADDITIONAL_EXTENSION_MAPPING = Collections.unmodifiableMap(new HashMap() {{ put(".properties", "text/plain"); @@ -48,7 +48,7 @@ private MimeTypeDetector() { throw new IllegalStateException("Static only class. No instances should exist for the class!"); } - private static int[] readDetectionBytes(@Nonnull InputStream is) throws IOException { + static int[] readDetectionBytes(@Nonnull InputStream is) throws IOException { if (!is.markSupported()) { // Trigger UnsupportedOperationException before reading the stream, no users should get there unless they hack with reflections is.reset(); @@ -66,24 +66,38 @@ private static int[] readDetectionBytes(@Nonnull InputStream is) throws IOExcept return bytes; } + static boolean isBinary(@Nonnull InputStream is) throws IOException { + int[] bytes = readDetectionBytes(is); + for (int b : bytes) { + if (b == 0) { + return true; + } + } + return false; + } + @Nullable static String guessContentTypeFromStream(@Nonnull InputStream is) throws IOException { int[] bytes = readDetectionBytes(is); if (bytes.length >= 8) { - if (bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4e && bytes[3] == 0x47 // 4 bytes break - && bytes[4] == 0x0d && bytes[5] == 0x0a && bytes[6] == 0x1a && bytes[7] == 0x0a) { + if (bytes[0] == 0x89 && bytes[1] == 'P' && bytes[2] == 'N' && bytes[3] == 'G' // 4 bytes break + && bytes[4] == '\r' && bytes[5] == '\n' && bytes[6] == 0x1a && bytes[7] == '\n') { return "image/png"; } } if (bytes.length >= 4) { - if (bytes[0] == 0x50 && bytes[1] == 0x4b && bytes[2] == 0x03 && bytes[3] == 0x04) { + if (bytes[0] == 'P' && bytes[1] == 'K' && bytes[2] == 0x03 && bytes[3] == 0x04) { // ZIPs if (bytes.length >= 7 && bytes[4] == 0x14 && bytes[5] == 0x00 && bytes[6] == 0x08) { return "application/java-archive"; } return "application/zip"; } - if (bytes[0] == 0x25 && bytes[1] == 0x50 && bytes[2] == 0x44 && bytes[3] == 0x46) { + if (bytes[0] == 'P' && bytes[1] == 'K' && bytes[2] == 0x05 && bytes[3] == 0x06) { + // Zero-length ZIP + return "application/zip"; + } + if (bytes[0] == '%' && bytes[1] == 'P' && bytes[2] == 'D' && bytes[3] == 'F' && isBinary(is)) { return "application/pdf"; } if (bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF) { @@ -94,6 +108,11 @@ static String guessContentTypeFromStream(@Nonnull InputStream is) throws IOExcep } } } + if (bytes.length >= 2) { + if (bytes[0] == 'B' && bytes[1] == 'M' && isBinary(is)) { + return "image/bmp"; + } + } return null; } @@ -139,6 +158,6 @@ public static String detect(@Nonnull final ByteSource source, @Nullable final St type = detectByExtensionInternal(resourceName); } } - return type == null ? UNKNOWN_TYPE : type; + return type == null ? isBinary(source.openStream()) ? UNKNOWN_TYPE : "text/plain" : type; } } diff --git a/src/test/java/com/epam/reportportal/utils/MimeTypeDetectorTest.java b/src/test/java/com/epam/reportportal/utils/MimeTypeDetectorTest.java index bdc1bd34..0547f0e7 100644 --- a/src/test/java/com/epam/reportportal/utils/MimeTypeDetectorTest.java +++ b/src/test/java/com/epam/reportportal/utils/MimeTypeDetectorTest.java @@ -28,7 +28,6 @@ public class MimeTypeDetectorTest { - @SuppressWarnings("unused") public static Iterable files() { return Arrays.asList( new Object[] { Paths.get("src/test/resources/pug/lucky.jpg").toFile(), "image/jpeg" }, @@ -52,8 +51,7 @@ public void test_mime_types_byte_source(File file, String expected) throws IOExc Assertions.assertEquals(expected, MimeTypeDetector.detect(Utils.getFileAsByteSource(file), file.getName())); } - @SuppressWarnings("unused") - public static Iterable binaryFiles() { + public static Iterable binaryFileTypes() { return Arrays.asList( new Object[] { Paths.get("src/test/resources/pug/lucky.jpg").toFile(), "image/jpeg" }, new Object[] { Paths.get("src/test/resources/pug/unlucky.jpg").toFile(), "image/jpeg" }, @@ -65,7 +63,7 @@ public static Iterable binaryFiles() { } @ParameterizedTest - @MethodSource("binaryFiles") + @MethodSource("binaryFileTypes") public void test_mime_types_files_by_content_only(File file, String expected) throws IOException { File testFile = Files.createTempFile("test_tmp_", null).toFile(); try (InputStream is = new FileInputStream(file)) { @@ -76,7 +74,6 @@ public void test_mime_types_files_by_content_only(File file, String expected) th Assertions.assertEquals(expected, MimeTypeDetector.detect(testFile)); } - @SuppressWarnings("unused") public static Iterable binaryFilesFallback() { return Arrays.asList( new Object[] { Paths.get("src/test/resources/pug/lucky.jpg").toFile(), "image/jpeg" }, @@ -96,4 +93,23 @@ public void test_mime_types_files_by_content_only_fallback(File file, String exp } Assertions.assertEquals(expected, MimeTypeDetector.guessContentTypeFromStream(Utils.getFileAsByteSource(testFile).openStream())); } + + public static Iterable binaryFiles() { + return Arrays.asList( + new Object[] { Paths.get("src/test/resources/pug/lucky.jpg").toFile(), true }, + new Object[] { Paths.get("src/test/resources/pug/unlucky.jpg").toFile(), true }, + new Object[] { Paths.get("src/test/resources/files/image.png").toFile(), true }, + new Object[] { Paths.get("src/test/resources/files/demo.zip").toFile(), true }, + new Object[] { Paths.get("src/test/resources/files/test.jar").toFile(), true }, + new Object[] { Paths.get("src/test/resources/files/test.pdf").toFile(), true }, + new Object[] { Paths.get("src/test/resources/files/test.bin").toFile(), true }, + new Object[] { Paths.get("src/test/resources/files/proxy_auth_response.txt").toFile(), false } + ); + } + + @ParameterizedTest + @MethodSource("binaryFiles") + public void test_is_binary(File file, boolean expected) throws IOException { + Assertions.assertEquals(MimeTypeDetector.isBinary(Utils.getFileAsByteSource(file).openStream()), expected); + } }