diff --git a/.travis.yml b/.travis.yml index 052b62b..5f441f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,10 @@ env: - TRAVIS_JDK=adopt-openj9@1.11.28-0 before_install: curl -Ls https://git.io/jabba | bash && . ~/.jabba/jabba.sh -install: jabba install "$TRAVIS_JDK" && jabba use $_ && java -Xmx32m -version + +install: + - jabba install "$TRAVIS_JDK" && jabba use $_ && java -Xmx32m -version + - mvn dependency:resolve stages: - validations @@ -19,8 +22,9 @@ stages: jobs: include: - stage: validations - script: mvn -B clean install -DskipTests=true name: "Code validations (format, binary compatibilty, whitesource, etc.)" + script: mvn -B clean install -DskipTests=true -Pcheckstyle + - script: mvn -B clean install -DskipTests=true -Pchecker cache: directories: diff --git a/build/checker/stubs/java.lang.Class.astub b/build/checker/stubs/java.lang.Class.astub new file mode 100644 index 0000000..3db6bea --- /dev/null +++ b/build/checker/stubs/java.lang.Class.astub @@ -0,0 +1,8 @@ +package java.lang; + +import org.checkerframework.checker.nullness.qual.*; + +public class Class { + // it *can* return null, but we only use it for loggers. + public @NonNull String getCanonicalName(); +} diff --git a/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactory.java b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactory.java index 7079f8e..0456f01 100644 --- a/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactory.java +++ b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactory.java @@ -16,6 +16,7 @@ package de.bmarwell.zchunk.compression.api; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import de.bmarwell.zchunk.compression.algo.unknown.UnknownAlgorithm; @@ -23,14 +24,18 @@ import java.util.AbstractMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.stream.Stream; +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.Nullable; public final class CompressionAlgorithmFactory { - private static final Package THIS_PACKAGE = CompressionAlgorithmFactory.class.getPackage(); - private static final String ROOT_PACKAGE = THIS_PACKAGE.getName().replaceAll("\\.api$", ".algo"); + // do not change this as this would break plugins. + private static final String ROOT_PACKAGE = "io.github.zchunk.compression"; private CompressionAlgorithmFactory() { // util class. @@ -70,17 +75,18 @@ private static Map> getTypeMappings() { private static class ResourceHolder { - private static ResourceHolder INSTANCE = null; + private static @Nullable ResourceHolder INSTANCE = null; private final List> implementations; private final Map> typeMapping; public ResourceHolder(final String rootPackage) { - this.implementations = ReflectionUtil.loadImplementations(rootPackage, CompressionAlgorithm.class); + this.implementations = loadImplementations(rootPackage, CompressionAlgorithm.class); this.typeMapping = loadTypeMapping(this.implementations); } + @EnsuresNonNull({"INSTANCE"}) public static ResourceHolder newInstance(final String rootPackage) { if (INSTANCE == null) { INSTANCE = new ResourceHolder(rootPackage); @@ -89,9 +95,32 @@ public static ResourceHolder newInstance(final String rootPackage) { return INSTANCE; } - private Map> loadTypeMapping(final List> implementations) { - return implementations.stream() - .map(CompressionAlgorithmFactory::mapEntryOrNull) + /** + * Will try to load classes implementing clazz, from any package below rootpackage. + * + * @param rootPackage + * the root package to search in. + * @param clazz + * the class which should be implemented by the found classes. + * @param + * the class type. + * @return a list of classes implementing T / clazz. + */ + public List> loadImplementations(@UnderInitialization ResourceHolder this, + final String rootPackage, + final Class clazz) { + final List> classes = ReflectionUtil.getClasses(rootPackage, clazz); + + return classes.stream() + .filter(ReflectionUtil.classImplementsCompressionAlgorithm(clazz)) + .collect(toList()); + } + + private Map> loadTypeMapping(@UnderInitialization ResourceHolder this, + final List> implementations) { + final Stream<@Nullable Entry>> entryStream = implementations.stream() + .map(CompressionAlgorithmFactory::mapEntryOrNull); + return entryStream .filter(Objects::nonNull) .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); } diff --git a/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/internal/ReflectionUtil.java b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/internal/ReflectionUtil.java index d2518fd..0eeb5de 100644 --- a/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/internal/ReflectionUtil.java +++ b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/internal/ReflectionUtil.java @@ -17,7 +17,6 @@ package de.bmarwell.zchunk.compression.api.internal; import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; import java.io.File; @@ -33,7 +32,6 @@ import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; -import org.checkerframework.checker.nullness.qual.Nullable; public final class ReflectionUtil { @@ -43,27 +41,7 @@ private ReflectionUtil() { // util } - /** - * Will try to load classes implementing clazz, from any package below rootpackage. - * - * @param rootPackage - * the root package to search in. - * @param clazz - * the class which should be implemented by the found classes. - * @param - * the class type. - * @return a list of classes implementing T / clazz. - */ - public static List> loadImplementations(final String rootPackage, final Class clazz) { - final List> classes = getClasses(rootPackage, clazz); - - return classes.stream() - .filter(classImplementsCompressionAlgorithm(clazz)) - .collect(toList()); - } - - - private static List> getClasses(final String rootPackage, final Class targetClazz) { + public static List> getClasses(final String rootPackage, final Class targetClazz) { try { final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); final String path = rootPackage.replace('.', '/'); @@ -84,7 +62,6 @@ private static List> getClasses(final String rootPackage, final Cla } } - /** * Recursive method used to find all classes in a given directory and subdirs. * @@ -118,27 +95,27 @@ private static List> findClasses(final String packageName, final Cl } if (file.getName().endsWith(".class")) { - final @Nullable Class aClass = loadClass(packageName, clazzType, file); - if (aClass != null) { - return singletonList(aClass); - } + return loadClass(packageName, clazzType, file) + .map(Collections::singletonList) + .orElseGet(Collections::emptyList); } return emptyList(); } - @Nullable - private static Class loadClass(final String packageName, final Class clazzType, final File file) { + private static Optional> loadClass(final String packageName, final Class clazzType, final File file) { try { final Class aClass = Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)); if (classImplementsCompressionAlgorithm(clazzType).test(aClass)) { - return (Class) aClass; + @SuppressWarnings("unchecked") + final Class castedClass = (Class) aClass; + return Optional.ofNullable(castedClass); } } catch (final ClassNotFoundException e) { LOG.log(Level.WARNING, e, () -> String.format("Class file [%s] found, but unable to create instance.", file.getAbsolutePath())); } - return null; + return Optional.empty(); } private static List getListFromArray(final T[] input) { @@ -165,7 +142,7 @@ public static Optional newInstance(final Class clazz) { } } - private static Predicate> classImplementsCompressionAlgorithm(final Class type) { + public static Predicate> classImplementsCompressionAlgorithm(final Class type) { return clazz -> getListFromArray(type.getInterfaces()).contains(type); } diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkConstants.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkConstants.java index 0489827..727643f 100644 --- a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkConstants.java +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkConstants.java @@ -21,12 +21,15 @@ import de.bmarwell.zchunk.compressedint.CompressedIntUtil; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.Provider; +import java.security.Provider.Service; import java.util.Arrays; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; public final class ZChunkConstants { @@ -50,13 +53,13 @@ private static int getLargestHashSize() { final String digestClassName = MessageDigest.class.getSimpleName(); final String aliasPrefix = "Alg.Alias." + digestClassName + "."; - return Arrays.stream(getProviders()) + final Stream<@Nullable MessageDigest> messageDigestStream = Arrays.stream(getProviders()) .flatMap(prov -> { final Set algorithms = new HashSet<>(0); prov.getServices().stream() .filter(s -> digestClassName.equalsIgnoreCase(s.getType())) - .map(Provider.Service::getAlgorithm) + .map(Service::getAlgorithm) .collect(Collectors.toCollection(() -> algorithms)); prov.keySet().stream() @@ -67,16 +70,25 @@ private static int getLargestHashSize() { return algorithms.stream(); }) - .map(algo -> { - try { - return MessageDigest.getInstance(algo); - } catch (NoSuchAlgorithmException e) { - return null; - } - }) - .filter(Objects::nonNull) + .map(ZChunkConstants::instanceOrNull); + + return toNonNullStream(messageDigestStream) .mapToInt(MessageDigest::getDigestLength) .max().orElse(512 / 8); } + + } + + @SuppressWarnings("nullness") + private static Stream<@NonNull MessageDigest> toNonNullStream(final Stream<@Nullable MessageDigest> messageDigestStream) { + return messageDigestStream.filter(Objects::nonNull); + } + + private static @Nullable MessageDigest instanceOrNull(final String algo) { + try { + return MessageDigest.getInstance(algo); + } catch (final NoSuchAlgorithmException e) { + return null; + } } } diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactory.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactory.java index a9451de..46f86ec 100644 --- a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactory.java +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactory.java @@ -90,7 +90,7 @@ public static ZChunkHeader fromStream(final InputStream byteStream) { final ZChunkHeaderPreface headerPreface = readHeaderPreface(completeHeader, lead); - final ZChunkHeaderIndex index = null; + final ZChunkHeaderIndex index = readHeaderIndex(completeHeader, lead, headerPreface); final ZChunkHeaderSignatures signature = ImmutableZChunkHeaderSignatures.builder() .signatureCount(CompressedIntFactory.valueOf(0L)) .build(); diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkIndexParser.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkIndexParser.java index ccc0979..4e11d7d 100644 --- a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkIndexParser.java +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkIndexParser.java @@ -32,6 +32,9 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; public final class ZChunkIndexParser { @@ -46,8 +49,8 @@ public final class ZChunkIndexParser { private long dictLengthOffset = -1L; private long dictUncompressedLengthOffset = -1L; private long chunkStreamOffset = -1L; - private CompressedInt chunkCount; - private IndexChecksumType chunkChecksumType; + private @Nullable CompressedInt chunkCount; + private @Nullable IndexChecksumType chunkChecksumType; private ZChunkIndexParser(final byte[] completeHeader, final ZChunkHeaderLead lead, final ZChunkHeaderPreface preface) { this.completeHeader = completeHeader; @@ -72,6 +75,7 @@ public CompressedInt readIndexSize() { } } + @EnsuresNonNull("chunkChecksumType") public CompressedInt readIndexCksumType() { if (this.cksumTypeOffset == -1L) { readIndexSize(); @@ -89,6 +93,7 @@ public CompressedInt readIndexCksumType() { } } + @EnsuresNonNull("chunkCount") public CompressedInt readChunkCount() { if (this.chunkCountOffset == -1) { readIndexCksumType(); @@ -122,15 +127,18 @@ public byte[] readDictStream() { } public byte[] readDictChecksum() { - if (this.dictChecksumOffest == -1L) { + if (this.dictChecksumOffest == -1L || null == this.chunkChecksumType) { readDictStream(); } try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.completeHeader)) { bis.skip(this.dictChecksumOffest); - // TODO. Which checksum does the dict use? chunk or header checksum? - final int dictChecksumLength = this.chunkChecksumType.actualChecksumLength(); + // safe to assume, as it is never set back to null. + @SuppressWarnings("nullness") + @NonNull + final IndexChecksumType chunkChecksumType = this.chunkChecksumType; + final int dictChecksumLength = chunkChecksumType.actualChecksumLength(); final byte[] dictChecksum = new byte[dictChecksumLength]; bis.read(dictChecksum); @@ -177,11 +185,20 @@ public CompressedInt readUncompressedDictLength() { } public List readChunkInfos() { - if (this.chunkStreamOffset == -1L) { + if (this.chunkStreamOffset == -1L || null == this.chunkChecksumType || null == this.chunkCount) { readUncompressedDictLength(); } - if (this.chunkCount.getValue().equals(BigInteger.ZERO)) { + // safe to assume, as it is never set back to null. + @SuppressWarnings("nullness") + @NonNull + final IndexChecksumType chunkChecksumType = this.chunkChecksumType; + // safe to assume, as it is never set back to null. + @SuppressWarnings("nullness") + @NonNull + final CompressedInt chunkCount = this.chunkCount; + + if (chunkCount.getValue().equals(BigInteger.ZERO)) { return emptyList(); } @@ -189,7 +206,7 @@ public List readChunkInfos() { long currentOffset = 0; // first chunk is the dict chunk. - for (long chunkNumber = 0; chunkNumber < this.chunkCount.getLongValue() - 1L; chunkNumber++) { + for (long chunkNumber = 0; chunkNumber < chunkCount.getLongValue() - 1L; chunkNumber++) { try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.completeHeader)) { bis.skip(this.chunkStreamOffset + currentOffset); if (this.preface.getPrefaceFlags().contains(PrefaceFlag.HAS_DATA_STREAMS)) { @@ -197,7 +214,7 @@ public List readChunkInfos() { throw new UnsupportedOperationException("data streams not implemented."); } - final byte[] chunkChecksum = new byte[this.chunkChecksumType.actualChecksumLength()]; + final byte[] chunkChecksum = new byte[chunkChecksumType.actualChecksumLength()]; bis.read(chunkChecksum); final CompressedInt compressedChunkLength = CompressedIntFactory.readCompressedInt(bis); @@ -210,7 +227,7 @@ public List readChunkInfos() { .chunkUncompressedLength(uncompressedChunkLength) .build()); - currentOffset += this.chunkChecksumType.actualChecksumLength() + currentOffset += chunkChecksumType.actualChecksumLength() + compressedChunkLength.getCompressedBytes().length + uncompressedChunkLength.getCompressedBytes().length; } catch (final IOException ioEx) { diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkLeadParser.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkLeadParser.java index db7bbeb..dc942d6 100644 --- a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkLeadParser.java +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkLeadParser.java @@ -27,11 +27,14 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; public class ZChunkLeadParser { private final byte[] leadBytes = new byte[MAX_LEAD_SIZE]; - private HeaderChecksumType checksumType; + private @Nullable HeaderChecksumType checksumType; private long headerSizeOffset; private long headerChecksumOffset; @@ -42,8 +45,11 @@ public class ZChunkLeadParser { * the bytes belonging to the lead. */ private ZChunkLeadParser(final byte[] leadBytes) { - if (leadBytes.length < MAX_LEAD_SIZE) { - throw new IllegalArgumentException(""); + final int length = leadBytes.length; + + if (length < MAX_LEAD_SIZE) { + final String message = String.format("lead bytes are too short with [%d] bytes. Need at least [%d] bytes.", length, MAX_LEAD_SIZE); + throw new IllegalArgumentException(message); } System.arraycopy(leadBytes, 0, this.leadBytes, 0, MAX_LEAD_SIZE); @@ -96,6 +102,7 @@ public byte[] readLeadId() { } } + @EnsuresNonNull("checksumType") public CompressedInt readLeadCksumType() { try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.leadBytes)) { final long skip = bis.skip(FILE_MAGIC.length); @@ -114,7 +121,7 @@ public CompressedInt readLeadCksumType() { } public CompressedInt readHeaderSize() { - if (this.headerSizeOffset == -1L) { + if (this.headerSizeOffset == -1L || null == this.checksumType) { readLeadCksumType(); } @@ -130,11 +137,15 @@ public CompressedInt readHeaderSize() { } public byte[] readHeaderChecksum() { - if (this.headerChecksumOffset == -1L) { + if (this.headerChecksumOffset == -1L || null == this.checksumType) { readHeaderSize(); } - final int cksumLength = this.checksumType.getDigestLength(); + // as checksumType is never set back to null, this is safe to assume. + @SuppressWarnings("nullness") + @NonNull + final HeaderChecksumType checksumType = this.checksumType; + final int cksumLength = checksumType.getDigestLength(); try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.leadBytes)) { bis.skip(this.headerChecksumOffset); diff --git a/pom.xml b/pom.xml index 0ab4277..e0c6f14 100644 --- a/pom.xml +++ b/pom.xml @@ -38,9 +38,12 @@ UTF-8 + 2.8.1 2.7.5 5.4.0 + + ${maven.multiModuleProjectDirectory}/build/checker/stubs @@ -113,6 +116,12 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M2 + + org.apache.maven.plugins @@ -144,19 +153,146 @@ org.apache.maven.plugins - maven-checkstyle-plugin - 3.0.0 - - - checkstyle-check - validate - - check - - - + maven-enforcer-plugin + + + + [3.3.1,) + + + + [1.8,) + + + + + org.apache.maven + com.google + org.slf4j + + + + + + + + checkstyle + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0.0 + + + checkstyle-check + validate + + check + + + + + + + + + + checker + + 2.5.6 + + + + org.checkerframework + checker-qual + ${dependency.checker.version} + true + + + org.checkerframework + checker + ${dependency.checker.version} + true + + + org.checkerframework + jdk8 + ${dependency.checker.version} + true + + + + org.immutables + value + ${dependency.immutables.version} + + + + + + + maven-enforcer-plugin + + + + enforce + + + + + + + + + + maven-dependency-plugin + + + + properties + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + 512m + + + org.immutables.value.internal.$processor$.$Processor + + + org.checkerframework.checker.nullness.NullnessChecker + + + org.checkerframework.checker.tainting.TaintingChecker + + + + -Xbootclasspath/p:${org.checkerframework:jdk8:jar} + + -Astubs=${checker.stubs.dir} + + -AskipDefs=^de\.bmarwell\.zchunk\..*\.ReflectionUtil$|io\.github\.zchunk\..*\.ReflectionUtil$ + |^de\.bmarwell\.zchunk\..*\.ChecksumUtil$|^io\.github\.zchunk\..*\.ChecksumUtil$ + |^.*Test$ + + + + + + + + +