diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7d99dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# maven +**/target + +# intellij +.idea/** + +# intellij project files +*.iml +**/*.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/compressedint/pom.xml b/compressedint/pom.xml new file mode 100644 index 0000000..7d72d65 --- /dev/null +++ b/compressedint/pom.xml @@ -0,0 +1,52 @@ + + + + + + zchunk-parent + de.bmarwell.zchunk + 1.0.0-SNAPSHOT + + 4.0.0 + + compressedint + + + + + org.immutables + value + provided + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + diff --git a/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/AbstractCompressedInt.java b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/AbstractCompressedInt.java new file mode 100644 index 0000000..8ee5581 --- /dev/null +++ b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/AbstractCompressedInt.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compressedint; + +import java.math.BigInteger; +import java.util.StringJoiner; +import org.immutables.value.Value; + +@Value.Immutable +public abstract class AbstractCompressedInt implements CompressedInt { + + @Override + @Value.Lazy + public BigInteger getValue() { + return CompressedIntUtil.decompress(getCompressedBytes()); + } + + @Override + @Value.Lazy + public long getLongValue() { + return getValue().longValueExact(); + } + + @Override + @Value.Lazy + public long getUnsignedLongValue() { + return getValue().longValue(); + } + + @Override + @Value.Lazy + public int getIntValue() { + return getValue().intValueExact(); + } + + @Override + public String toString() { + return new StringJoiner(", ", CompressedInt.class.getSimpleName() + "[", "]") + .add("compressedBytes=" + new BigInteger(1, getCompressedBytes()).toString(16)) + .add("unsignedValue=" + getValue().toString()) + .toString(); + } +} diff --git a/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedInt.java b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedInt.java new file mode 100644 index 0000000..eaa8056 --- /dev/null +++ b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedInt.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compressedint; + +import java.math.BigInteger; +import org.immutables.value.Value; + +public interface CompressedInt { + + byte[] getCompressedBytes(); + + /** + * If you have few experience with unsigned values in java, consider using this value instead. + * + * @return a biginteger which will always output a positive value. + */ + @Value.Lazy + BigInteger getValue(); + + @Value.Lazy + long getLongValue(); + + @Value.Lazy + long getUnsignedLongValue(); + + @Value.Lazy + int getIntValue(); +} diff --git a/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedIntFactory.java b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedIntFactory.java new file mode 100644 index 0000000..bf9af56 --- /dev/null +++ b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedIntFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compressedint; + +import java.io.IOException; +import java.io.InputStream; + +public final class CompressedIntFactory { + + private CompressedIntFactory() { + // util class + } + + public static CompressedInt fromCompressedBytes(final byte[] input) { + if (input.length > CompressedIntUtil.MAX_COMPRESSED_INT_LENGTH) { + throw new IllegalArgumentException( + "Input length [" + input.length + "] bytes is too large! Max allowed: " + CompressedIntUtil.MAX_COMPRESSED_INT_LENGTH); + } + + return ImmutableCompressedInt.builder() + .compressedBytes(input) + .build(); + } + + /** + * Convert an unsigned long to a compressed int. + * + *

Hint: If your long yields -1L, it's instead {@code 0xffffffffffffffff}. But java treats it as + * signed long.

+ * + * @param unsignedLongValue + * a long value which gets interpreted as unsigned. + * @return a compressedInt. + */ + public static CompressedInt valueOf(final long unsignedLongValue) { + final byte[] unsignedBytes = CompressedIntUtil.compress(unsignedLongValue); + + return fromCompressedBytes(unsignedBytes); + } + + public static CompressedInt readCompressedInt(final InputStream inputStream) throws IOException { + int currentByte; + int byteCounter = 0; + final byte[] buffer = new byte[CompressedIntUtil.MAX_COMPRESSED_INT_LENGTH]; + + while ((currentByte = inputStream.read()) != -1) { + if (byteCounter >= CompressedIntUtil.MAX_COMPRESSED_INT_LENGTH) { + throw new IllegalArgumentException("CompressedInt too large. Giving up after reading [" + byteCounter + "] bytes."); + } + + buffer[byteCounter] = (byte) currentByte; + + if ((currentByte & CompressedIntUtil.COMPRESSED_INT_LAST_BYTE_FLAG) == CompressedIntUtil.COMPRESSED_INT_LAST_BYTE_FLAG) { + break; + } + + byteCounter++; + } + + final byte[] read = new byte[byteCounter + 1]; + System.arraycopy(buffer, 0, read, 0, byteCounter + 1); + + return fromCompressedBytes(read); + } +} diff --git a/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedIntUtil.java b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedIntUtil.java new file mode 100644 index 0000000..3f53340 --- /dev/null +++ b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/CompressedIntUtil.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compressedint; + +import static java.math.BigInteger.ONE; + +import java.math.BigInteger; + +public final class CompressedIntUtil { + + public static final int COMPRESSED_INT_LAST_BYTE_FLAG = 0b10000000; + + /** + * The maximum value is 0xffffffffffffffff (which equals signed -1). + */ + public static final long MAX_VALUE = 0xffffffffffffffffL; + + /** + * This bitmask is used to read in an unsigned long as positive value. + */ + public static final BigInteger UNSIGNED_LONG_MASK = ONE.shiftLeft(Long.SIZE).subtract(ONE); + + /** + * Since an (unsigned) long (64 bits / 8 byte) is the maximum length we allow at this point, + * the max length of a compressed integer is 10 bytes. + * 10 bytes are needed to encode -1L (or 0xffffffffffffffff) as compressed int. + */ + public static final int MAX_COMPRESSED_INT_LENGTH = (Long.SIZE / 7) + 1; + + private CompressedIntUtil() { + // private util + } + + + public static byte[] compress(final long unsignedIntValue) { + long modValue = unsignedIntValue; + final byte[] tmp = new byte[MAX_COMPRESSED_INT_LENGTH]; + int byteIndex = 0; + + while (true) { + // get the rightmost 7 bits + final byte currentByte = (byte) (modValue & 0b01111111); + // unsigned(!) shift by seven bits. + modValue >>>= 7L; + tmp[byteIndex] = currentByte; + + if (modValue == 0L) { + // this is the last byte we encoded. Make sure we set the last-byte-flag. + tmp[byteIndex] |= COMPRESSED_INT_LAST_BYTE_FLAG; + break; + } + + byteIndex++; + } + + final byte[] out = new byte[byteIndex + 1]; + System.arraycopy(tmp, 0, out, 0, byteIndex + 1); + + return out; + } + + public static BigInteger decompress(final byte[] compressedUnsignedInt) { + if (compressedUnsignedInt.length > MAX_COMPRESSED_INT_LENGTH) { + throw new IllegalArgumentException("Compressed int too big!"); + } + + long result = 0L; + int shift = 0; + + for (final byte b : compressedUnsignedInt) { + final byte leadingZero = (byte) (b & ~COMPRESSED_INT_LAST_BYTE_FLAG); + + result |= leadingZero << shift; + shift += 7; + } + + return BigInteger.valueOf(result).and(UNSIGNED_LONG_MASK); + } + +} diff --git a/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/package-info.java b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/package-info.java new file mode 100644 index 0000000..3c93bb2 --- /dev/null +++ b/compressedint/src/main/java/de/bmarwell/zchunk/compressedint/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@Value.Style(stagedBuilder = true, jdkOnly = true) +package de.bmarwell.zchunk.compressedint; + +import org.immutables.value.Value; diff --git a/compressedint/src/test/java/de/bmarwell/zchunk/compressedint/CompressedIntTest.java b/compressedint/src/test/java/de/bmarwell/zchunk/compressedint/CompressedIntTest.java new file mode 100644 index 0000000..73e04bf --- /dev/null +++ b/compressedint/src/test/java/de/bmarwell/zchunk/compressedint/CompressedIntTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compressedint; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.logging.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CompressedIntTest { + + private static final Logger LOG = Logger.getLogger(CompressedIntTest.class.getCanonicalName()); + + private static final byte[] CP_394 = new byte[]{(byte) 0b00001010, (byte) 0b10000011}; + + public static String byteArrayToBinaryString(final byte[] input) { + return new BigInteger(1, input).toString(2); + } + + @Test + public void testToCompressedInt() { + final CompressedInt ci = CompressedIntFactory.valueOf(394L); + + final String binaryString = byteArrayToBinaryString(ci.getCompressedBytes()); + + Assertions.assertEquals("101010000011", binaryString); + } + + @Test + public void testToCompressedInt_max() { + final CompressedInt bytes = CompressedIntFactory.valueOf(Integer.MAX_VALUE); + + final String binaryString = byteArrayToBinaryString(bytes.getCompressedBytes()); + + Assertions.assertEquals("111111101111111011111110111111110000111", binaryString); + } + + @Test + public void testToCompressedInt_ulong_max() { + final byte[] ulongAsBytes = new byte[8]; + Arrays.fill(ulongAsBytes, (byte) 0xff); + final long unsignedLongValue = new BigInteger(1, ulongAsBytes).longValue(); + + final CompressedInt bytes = CompressedIntFactory.valueOf(unsignedLongValue); + + final String binaryString = byteArrayToBinaryString(bytes.getCompressedBytes()); + + Assertions.assertEquals("1111111011111110111111101111111011111110111111101111111011111110111111110000001", binaryString); + } + + @Test + public void testReadCompressedInt() { + final byte[] maxLong = new byte[8]; + Arrays.fill(maxLong, (byte) 0xff); + final CompressedInt compressedInt = CompressedIntFactory.fromCompressedBytes(maxLong); + + Assertions.assertAll( + () -> Assertions.assertEquals("18446744073709551615", compressedInt.getValue().toString()) + ); + } + + @Test + public void testReadCompressedInt_maxValue() { + // 1111111 01111111 01111111 01111111 01111111 01111111 01111111 011111110 11111111 0000001 + // repeated max value on purpose: guard against changes. + final byte[] bytes = new byte[]{ + 0b1111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, (byte) 0b011111110, (byte) 0b11111111, 0b0000001 + }; + + final CompressedInt compressedInt = CompressedIntFactory.fromCompressedBytes(bytes); + + Assertions.assertAll( + () -> Assertions.assertEquals("18446744073709551615", compressedInt.getValue().toString()) + ); + } + + @Test + public void testCompressedIntMaxLength() { + Assertions.assertAll( + () -> Assertions.assertEquals(10, CompressedIntUtil.MAX_COMPRESSED_INT_LENGTH), + () -> Assertions.assertThrows(IllegalArgumentException.class, () -> CompressedIntFactory.fromCompressedBytes(new byte[11])) + ); + } + + @Test + public void testToCompressedInt_unsigned_394() { + final CompressedInt ci = CompressedIntFactory.valueOf(394); + + Assertions.assertAll( + () -> Assertions.assertEquals(2, ci.getCompressedBytes().length), + () -> Assertions.assertArrayEquals(CP_394, ci.getCompressedBytes()) + ); + } + + @Test + public void testFromBytes_6582() { + final CompressedInt compressedInt = CompressedIntFactory.fromCompressedBytes(new byte[]{0x65, (byte) 0x82}); + + Assertions.assertAll( + () -> Assertions.assertEquals(2L, compressedInt.getCompressedBytes().length), + () -> Assertions.assertEquals(357L, compressedInt.getLongValue()) + ); + } + + @Test + public void testToUnsignedInt_zero() { + final byte[] input = new byte[]{(byte) 0b10000000}; + final CompressedInt anInt = CompressedIntFactory.fromCompressedBytes(input); + final long unsignedLong = anInt.getIntValue(); + + Assertions.assertEquals(0L, unsignedLong); + } + + @Test + public void testToUnsignedInt_one() { + final byte[] input = new byte[]{(byte) 0b10000001}; + final CompressedInt anInt = CompressedIntFactory.fromCompressedBytes(input); + final long unsignedLong = anInt.getIntValue(); + + Assertions.assertEquals(1L, unsignedLong); + } + + @Test + public void testToUnsignedInt() { + final CompressedInt anInt = CompressedIntFactory.fromCompressedBytes(CP_394); + final long unsignedLong = anInt.getLongValue(); + + Assertions.assertEquals(394L, unsignedLong); + } + + @Test + public void testExceptionOnBigLongToInt() { + final CompressedInt compressedInt = CompressedIntFactory.valueOf(0xffffffff00ffffL); + + Assertions.assertThrows(ArithmeticException.class, compressedInt::getIntValue); + } + + @Test + public void testExceptionOnBigLongToLong() { + final CompressedInt compressedInt = CompressedIntFactory.valueOf(0xffffffff00ffffL); + + Assertions.assertThrows(ArithmeticException.class, compressedInt::getLongValue); + } + + @Test + public void testExceptionOnBigLongToUnsignedLong() { + final CompressedInt compressedInt = CompressedIntFactory.valueOf(-1L); + + Assertions.assertEquals(-1L, compressedInt.getUnsignedLongValue()); + } + + @Test + public void testExceptionOnBigLongToUnsignedLong_minus2() { + final CompressedInt compressedInt = CompressedIntFactory.valueOf(-2L); + + Assertions.assertEquals(-2L, compressedInt.getUnsignedLongValue()); + } +} diff --git a/compression/compression-api/pom.xml b/compression/compression-api/pom.xml new file mode 100644 index 0000000..e63e3ec --- /dev/null +++ b/compression/compression-api/pom.xml @@ -0,0 +1,65 @@ + + + + + 4.0.0 + + + de.bmarwell.zchunk + zchunk-parent + 1.0.0-SNAPSHOT + ../.. + + + compression-api + + + + de.bmarwell.zchunk + compressedint + 1.0.0-SNAPSHOT + + + + + org.immutables + value + provided + + + + org.checkerframework + checker-qual + provided + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + diff --git a/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/algo/unknown/UnknownAlgorithm.java b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/algo/unknown/UnknownAlgorithm.java new file mode 100644 index 0000000..2addfd0 --- /dev/null +++ b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/algo/unknown/UnknownAlgorithm.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compression.algo.unknown; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.compressedint.CompressedIntFactory; +import de.bmarwell.zchunk.compression.api.CompressionAlgorithm; +import java.io.InputStream; +import java.util.function.Function; + +public class UnknownAlgorithm implements CompressionAlgorithm { + + @Override + public CompressedInt getCompressionTypeValue() { + return CompressedIntFactory.valueOf(-1L); + } + + @Override + public String getName() { + return "unknown"; + } + + @Override + public Function getOutputStreamSupplier() { + throw new UnsupportedOperationException("not implemented"); + } +} diff --git a/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithm.java b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithm.java new file mode 100644 index 0000000..2fa042d --- /dev/null +++ b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithm.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compression.api; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import java.io.InputStream; +import java.util.function.Function; +import org.immutables.value.Value; + +@Value.Immutable +public interface CompressionAlgorithm { + + CompressedInt getCompressionTypeValue(); + + String getName(); + + Function getOutputStreamSupplier(); +} 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 new file mode 100644 index 0000000..b422d7a --- /dev/null +++ b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactory.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compression.api; + +import static java.util.stream.Collectors.toMap; + +import de.bmarwell.zchunk.compression.algo.unknown.UnknownAlgorithm; +import de.bmarwell.zchunk.compression.api.internal.ReflectionUtil; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +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"); + + private CompressionAlgorithmFactory() { + // util class. + } + + private static Map.@Nullable Entry> mapEntryOrNull(final Class clazz) { + return ReflectionUtil.newInstance(clazz) + .map(compInstance -> new AbstractMap.SimpleEntry<>(compInstance.getCompressionTypeValue().getLongValue(), clazz)) + .orElse(null); + } + + public static CompressionAlgorithm forType(final long compressionType) { + return Optional.ofNullable(getTypeMappings().get(compressionType)) + .flatMap(ReflectionUtil::newInstance) + .orElseGet(UnknownAlgorithm::new); + } + + /* Utility methods */ + + private static List> getImplementations() { + return ResourceHolder.newInstance(ROOT_PACKAGE).getImplementations(); + } + + private static Map> getTypeMappings() { + return ResourceHolder.newInstance(ROOT_PACKAGE).getTypeMapping(); + } + + + private static class ResourceHolder { + + private static ResourceHolder INSTANCE = null; + + private final List> implementations; + + private final Map> typeMapping; + + public ResourceHolder(final String rootPackage) { + this.implementations = ReflectionUtil.loadImplementations(rootPackage, CompressionAlgorithm.class); + this.typeMapping = loadTypeMapping(this.implementations); + } + + public static ResourceHolder newInstance(final String rootPackage) { + if (INSTANCE == null) { + INSTANCE = new ResourceHolder(rootPackage); + } + + return INSTANCE; + } + + private Map> loadTypeMapping(final List> implementations) { + return implementations.stream() + .map(CompressionAlgorithmFactory::mapEntryOrNull) + .filter(Objects::nonNull) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + public List> getImplementations() { + return this.implementations; + } + + public Map> getTypeMapping() { + return this.typeMapping; + } + } +} 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 new file mode 100644 index 0000000..e5d7ee7 --- /dev/null +++ b/compression/compression-api/src/main/java/de/bmarwell/zchunk/compression/api/internal/ReflectionUtil.java @@ -0,0 +1,152 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; +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 { + + private static final Logger LOG = Logger.getLogger(ReflectionUtil.class.getCanonicalName()); + + private ReflectionUtil() { + // util + } + + 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) { + try { + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + final String path = rootPackage.replace('.', '/'); + final Enumeration resources = classLoader.getResources(path); + final List dirs = new ArrayList<>(); + + while (resources.hasMoreElements()) { + final URL resource = resources.nextElement(); + dirs.add(new File(resource.getFile())); + } + + return dirs.stream() + .map(dir -> findClasses(dir, rootPackage, targetClazz)) + .flatMap(Collection::stream) + .collect(toList()); + } catch (final IOException ioEx) { + return emptyList(); + } + } + + + /** + * Recursive method used to find all classes in a given directory and subdirs. + * + * @param directory + * The base directory + * @param packageName + * The package name for classes found inside the base directory + * @return The classes + */ + private static List> findClasses(final File directory, final String packageName, final Class clazzType) { + if (!directory.exists()) { + return Collections.emptyList(); + } + + final List files = getListFromArray(directory.listFiles()); + + return files.stream() + .map(file -> findClasses(packageName, clazzType, file)) + .flatMap(Collection::stream) + .collect(toList()); + } + + private static List> findClasses(final String packageName, final Class clazzType, final File file) { + if (file.isDirectory()) { + if (file.getName().contains(".")) { + // bad match. + return emptyList(); + } + + return findClasses(file, packageName + "." + file.getName(), clazzType); + } + + if (file.getName().endsWith(".class")) { + final @Nullable Class aClass = loadClass(packageName, clazzType, file); + if (aClass != null) { + return singletonList(aClass); + } + } + + return emptyList(); + } + + @Nullable + private static Class 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; + } + } catch (final ClassNotFoundException e) { + LOG.log(Level.WARNING, e, () -> String.format("Class file [%s] found, but unable to create instance.", file.getAbsolutePath())); + } + + return null; + } + + private static List getListFromArray(final T[] input) { + return Optional.ofNullable(input) + .map(Arrays::asList) + .orElseGet(Collections::emptyList); + } + + public static Optional newInstance(final Class clazz) { + try { + return Optional.of(clazz.newInstance()); + } catch (final InstantiationException | IllegalAccessException e) { + LOG.log(Level.WARNING, e, () -> String.format("Unable to instantiate class [%s]. Skipping.", clazz)); + return Optional.empty(); + } + } + + private static Predicate> classImplementsCompressionAlgorithm(final Class type) { + return clazz -> getListFromArray(type.getInterfaces()).contains(type); + } + +} diff --git a/compression/compression-api/src/test/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactoryTest.java b/compression/compression-api/src/test/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactoryTest.java new file mode 100644 index 0000000..c96db2c --- /dev/null +++ b/compression/compression-api/src/test/java/de/bmarwell/zchunk/compression/api/CompressionAlgorithmFactoryTest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.compression.api; + +import de.bmarwell.zchunk.compression.algo.unknown.UnknownAlgorithm; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CompressionAlgorithmFactoryTest { + + @Test + public void testGetUnknown() { + final CompressionAlgorithm algorithm = CompressionAlgorithmFactory.forType(-1L); + + Assertions.assertEquals(algorithm.getClass(), UnknownAlgorithm.class); + } +} diff --git a/compression/compression-api/src/test/resources/logging.properties b/compression/compression-api/src/test/resources/logging.properties new file mode 100644 index 0000000..caa25eb --- /dev/null +++ b/compression/compression-api/src/test/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright 2019, the zchunk-java contributors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +# default log level +.level=FINE +# console handler settings +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tFT%1$tT.%1$tLZ] [%4$-6s] %2$s - %5$s%6$s%n +# class specific settings +de.bmarwell.zchunk.compression.level=FINER +# 3rd party +org.junit.platform.level=INFO diff --git a/fileformat/pom.xml b/fileformat/pom.xml new file mode 100644 index 0000000..303a899 --- /dev/null +++ b/fileformat/pom.xml @@ -0,0 +1,73 @@ + + + + + + zchunk-parent + de.bmarwell.zchunk + 1.0.0-SNAPSHOT + + 4.0.0 + + zchunk-fileformat + + + + + de.bmarwell.zchunk + compressedint + 1.0.0-SNAPSHOT + + + + + de.bmarwell.zchunk + compression-api + 1.0.0-SNAPSHOT + + + + + + org.immutables + value + provided + + + + org.checkerframework + checker-qual + provided + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/HeaderChecksumType.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/HeaderChecksumType.java new file mode 100644 index 0000000..e5cd704 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/HeaderChecksumType.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.StringJoiner; + +/** + * 0 = SHA-1 + * 1 = SHA-256 + */ +public enum HeaderChecksumType { + SHA1("SHA-1"), + SHA256("SHA-256"); + + private final String digestAlgorithm; + private final int digestLength; + + HeaderChecksumType(final String digestAlgorithm) { + try { + this.digestAlgorithm = digestAlgorithm; + this.digestLength = MessageDigest.getInstance(digestAlgorithm).getDigestLength(); + } catch (final NoSuchAlgorithmException algoEx) { + throw new IllegalArgumentException("Unable to create hashing algorithm: [" + digestAlgorithm + "]. Check your JVM settings.", algoEx); + } + } + + public int getDigestLength() { + return this.digestLength; + } + + public MessageDigest digest() { + try { + return MessageDigest.getInstance(this.digestAlgorithm); + } catch (final NoSuchAlgorithmException algoEx) { + throw new UnsupportedOperationException("Not implemented: [" + this.digestAlgorithm + "]."); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", HeaderChecksumType.class.getSimpleName() + "[", "]") + .add("digestAlgorithm=" + this.digestAlgorithm) + .add("digestLength=" + this.digestLength) + .add("ordinal=" + this.ordinal()) + .toString(); + } + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/IndexChecksumType.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/IndexChecksumType.java new file mode 100644 index 0000000..6636ae5 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/IndexChecksumType.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.StringJoiner; + +/** + * Current values: + * 0 = SHA-1 + * 1 = SHA-256 + * 2 = SHA-512 + * 3 = SHA-512/128 (first 128 bits of SHA-512 checksum) + */ +public enum IndexChecksumType { + SHA1("SHA-1", -1), + SHA256("SHA-256", -1), + SHA512("SHA-512", -1), + SHA512_128("SHA-512", 16); + + private final MessageDigest digestAlgorithm; + private final int length; + + IndexChecksumType(final String digestAlgorithm, final int length) { + try { + this.digestAlgorithm = MessageDigest.getInstance(digestAlgorithm); + if (length != -1) { + this.length = length; + } else { + this.length = this.digestAlgorithm.getDigestLength(); + } + } catch (final NoSuchAlgorithmException algoEx) { + throw new IllegalArgumentException("Unable to create hashing algorithm: [" + digestAlgorithm + "]. Check your JVM settings.", algoEx); + } + } + + public int actualChecksumLength() { + return this.length; + } + + public byte[] digest(final byte[] input) { + final byte[] digest = this.digestAlgorithm.digest(input); + + if (this.length != this.digestAlgorithm.getDigestLength()) { + final byte[] actualDigest = new byte[this.length]; + System.arraycopy(digest, 0, actualDigest, 0, this.length); + return actualDigest; + } + + return digest; + } + + @Override + public String toString() { + return new StringJoiner(", ", IndexChecksumType.class.getSimpleName() + "[", "]") + .add("digestAlgorithm=" + this.digestAlgorithm.getAlgorithm()) + .add("actualChecksumLength=" + this.length) + .add("ordinal=" + this.ordinal()) + .toString(); + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/OptionalElement.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/OptionalElement.java new file mode 100644 index 0000000..9ada22d --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/OptionalElement.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import java.math.BigInteger; +import org.immutables.value.Value; + +@Value.Immutable +public interface OptionalElement { + + CompressedInt getId(); + + CompressedInt getDataSize(); + + byte[] getData(); + + @Value.Derived + default long getTotalLength() { + return BigInteger.valueOf(getId().getCompressedBytes().length) + .add(BigInteger.valueOf(getDataSize().getCompressedBytes().length)) + // either this or getData().length + .add(getDataSize().getValue()) + .longValueExact(); + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/PrefaceFlag.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/PrefaceFlag.java new file mode 100644 index 0000000..6578eeb --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/PrefaceFlag.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.fileformat.util.ByteUtils; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/* + * Current flags are: + * bit 0: File has data streams + * bit 1: File has optional elements + */ +public enum PrefaceFlag { + HAS_DATA_STREAMS(0b00000001L), + HAS_OPTIONAL_ELEMENTS(0b00000010L); + + private final long bitflag; + + PrefaceFlag(final long flag) { + this.bitflag = flag; + } + + public static Set getPrefaceFlags(final CompressedInt ci) { + final AtomicReference remainingFlagLong = new AtomicReference<>(ci.getValue()); + + return getPrefaceFlags(remainingFlagLong); + } + + private static Set getPrefaceFlags(final AtomicReference remainingFlagLong) { + final Set foundFlags = Arrays.stream(PrefaceFlag.values()) + .filter(currentFlag -> ByteUtils.decrease(remainingFlagLong, currentFlag.getBitflag())) + .collect(Collectors.toSet()); + + if (remainingFlagLong.get().longValue() != 0L) { + throw new UnsupportedOperationException( + "Flags not supported yet: [" + ByteUtils.longToBinaryString(remainingFlagLong.get().longValue()) + "]."); + } + + return foundFlags; + } + + public long getBitflag() { + return this.bitflag; + } + + @Override + public String toString() { + return new StringJoiner(", ", PrefaceFlag.class.getSimpleName() + "[", "]") + .add("bitflag=" + this.bitflag) + .toString(); + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkConstants.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkConstants.java new file mode 100644 index 0000000..0489827 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkConstants.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import static java.security.Security.getProviders; + +import de.bmarwell.zchunk.compressedint.CompressedIntUtil; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public final class ZChunkConstants { + + private ZChunkConstants() { + // util + } + + public static class Header { + + public static final byte[] FILE_MAGIC = new byte[]{'\0', 'Z', 'C', 'K', '1'}; + + private static final int MAX_CHECKSUM_SIZE = getLargestHashSize(); + public static int MAX_LEAD_SIZE = FILE_MAGIC.length + // checksum type (ci) + + CompressedIntUtil.MAX_COMPRESSED_INT_LENGTH + // header size (ci) + + CompressedIntUtil.MAX_COMPRESSED_INT_LENGTH + + MAX_CHECKSUM_SIZE; + + private static int getLargestHashSize() { + final String digestClassName = MessageDigest.class.getSimpleName(); + final String aliasPrefix = "Alg.Alias." + digestClassName + "."; + + return Arrays.stream(getProviders()) + .flatMap(prov -> { + final Set algorithms = new HashSet<>(0); + + prov.getServices().stream() + .filter(s -> digestClassName.equalsIgnoreCase(s.getType())) + .map(Provider.Service::getAlgorithm) + .collect(Collectors.toCollection(() -> algorithms)); + + prov.keySet().stream() + .map(Object::toString) + .filter(k -> k.startsWith(aliasPrefix)) + .map(k -> String.format("\"%s\" -> \"%s\"", k.substring(aliasPrefix.length()), prov.get(k).toString())) + .collect(Collectors.toCollection(() -> algorithms)); + + return algorithms.stream(); + }) + .map(algo -> { + try { + return MessageDigest.getInstance(algo); + } catch (NoSuchAlgorithmException e) { + return null; + } + }) + .filter(Objects::nonNull) + .mapToInt(MessageDigest::getDigestLength) + .max().orElse(512 / 8); + } + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkFile.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkFile.java new file mode 100644 index 0000000..eede773 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkFile.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import org.immutables.value.Value; + + +@Value.Immutable +public interface ZChunkFile { + + ZChunkHeader getHeader(); + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeader.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeader.java new file mode 100644 index 0000000..a9f9053 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeader.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import org.immutables.value.Value; + +/** + * The header consists of four parts: + * + *
    + *
  • The lead: Everything necessary to validate the header
  • + *
  • The preface: Metadata about the zchunk file
  • + *
  • The index: Details about each chunk
  • + *
  • The signatures: Signatures used to sign the zchunk file
  • + *
+ */ +@Value.Immutable +public interface ZChunkHeader { + + ZChunkHeaderLead getLead(); + + ZChunkHeaderPreface getPreface(); + + ZChunkHeaderIndex getIndex(); + + ZChunkHeaderSignatures getSignatures(); + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderChunkInfo.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderChunkInfo.java new file mode 100644 index 0000000..c5d28a4 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderChunkInfo.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.fileformat.util.ByteUtils; +import java.util.Optional; +import java.util.StringJoiner; +import org.immutables.value.Value; + +/** + *
+ * (Chunk stream will only exist if flag 0 is set to 1)
+ * [+===================+================+===================+
+ * [| Chunk stream (ci) | Chunk checksum | Chunk length (ci) |
+ * [+===================+================+===================+
+ *
+ * +==========================+]
+ * | Uncompressed length (ci) |] ...
+ * +==========================+]
+ * 
+ */ + +@Value.Immutable +public abstract class ZChunkHeaderChunkInfo { + + public abstract long getCurrentIndex(); + + public abstract Optional getChunkStream(); + + public abstract byte[] getChunkChecksum(); + + public abstract CompressedInt getChunkLength(); + + public abstract CompressedInt getChunkUncompressedLength(); + + @Override + public String toString() { + return new StringJoiner(", ", ZChunkHeaderChunkInfo.class.getSimpleName() + "[", "]") + .add("index=" + getCurrentIndex()) + .add("chunkStream=" + ByteUtils.byteArrayToHexString(getChunkStream().orElse(new byte[0]))) + .add("chunkChecksum=" + ByteUtils.byteArrayToHexString(getChunkChecksum())) + .add("chunkLength=" + getChunkLength()) + .add("chunkUncompressedLength=" + getChunkUncompressedLength()) + .toString(); + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactory.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactory.java new file mode 100644 index 0000000..5cc936c --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactory.java @@ -0,0 +1,250 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import static de.bmarwell.zchunk.fileformat.ZChunkConstants.Header.MAX_LEAD_SIZE; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.compressedint.CompressedIntFactory; +import de.bmarwell.zchunk.compression.api.CompressionAlgorithm; +import de.bmarwell.zchunk.compression.api.CompressionAlgorithmFactory; +import de.bmarwell.zchunk.compression.api.ImmutableCompressionAlgorithm; +import de.bmarwell.zchunk.fileformat.err.InvalidFileException; +import de.bmarwell.zchunk.fileformat.parser.ZChunkIndexParser; +import de.bmarwell.zchunk.fileformat.parser.ZChunkLeadParser; +import de.bmarwell.zchunk.fileformat.parser.ZChunkPrefaceParser; +import de.bmarwell.zchunk.fileformat.util.OffsetUtil; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.math.BigInteger; +import java.nio.channels.Channels; +import java.util.Set; +import java.util.logging.Logger; + +public final class ZChunkHeaderFactory { + + private static final Logger LOG = Logger.getLogger(ZChunkHeaderFactory.class.getCanonicalName()); + + private ZChunkHeaderFactory() { + // + } + + /** + * Reads in a zchunk file. + * + *

The header part will stay in memory (heap).
+ * The data streams and/or chunks will be available as inputstream, but are not + * eagerly loaded into memory.

+ * + * @param input + * the input file. + * @return a {@link ZChunkFile} instance. + * @throws InvalidFileException + * if the input file is not a zchunk file. + * @throws NullPointerException + * if the input file is {@code null}. + */ + public static ZChunkFile fromFile(final File input) { + final ZChunkHeader header = getZChunkFileHeader(input); + + return ImmutableZChunkFile.builder().header(header).build(); + } + + private static ZChunkHeader getZChunkFileHeader(final File input) { + final ZChunkHeaderLead lead = readFileHeaderLead(input); + final byte[] completeHeader = readCompleteHeader(input, lead); + final ZChunkHeaderPreface preface = readHeaderPreface(completeHeader, lead); + final ZChunkHeaderIndex index = readHeaderIndex(completeHeader, lead, preface); + final ZChunkHeaderSignatures signatures = readSignatureIndex(completeHeader, lead, preface, index); + + return ImmutableZChunkHeader.builder() + .lead(lead) + .preface(preface) + .index(index) + .signatures(signatures) + .build(); + } + + public static ZChunkHeader fromStream(final InputStream byteStream) { + try { + final byte[] leadBytes = new byte[MAX_LEAD_SIZE]; + final int read = byteStream.read(leadBytes); + + if (read < MAX_LEAD_SIZE) { + throw new IllegalArgumentException("Unable to read enough bytes from bytestream!"); + } + + final ZChunkHeaderLead lead = readFileHeaderLead(leadBytes); + final byte[] completeHeader = new byte[OffsetUtil.getTotalHeaderSize(lead)]; + System.arraycopy(leadBytes, 0, completeHeader, 0, leadBytes.length); + final long bytesRemaining = OffsetUtil.getLeadLength(lead) - read; + + if (BigInteger.valueOf(bytesRemaining).compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) == 1) { + throw new IllegalStateException("Cannot read remaining [" + bytesRemaining + "] bytes. Value too large for integer."); + } + + final byte[] headerBytesRemaining = new byte[Math.toIntExact(bytesRemaining)]; + byteStream.read(headerBytesRemaining); + System.arraycopy(headerBytesRemaining, 0, completeHeader, leadBytes.length, headerBytesRemaining.length); + + final ZChunkHeaderPreface headerPreface = readHeaderPreface(completeHeader, lead); + + final ZChunkHeaderIndex index = null; + final ZChunkHeaderSignatures signature = ImmutableZChunkHeaderSignatures.builder() + .signatureCount(CompressedIntFactory.valueOf(0L)) + .build(); + + return ImmutableZChunkHeader.builder() + .lead(lead) + .preface(headerPreface) + .index(index) + .signatures(signature) + .build(); + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Unable to read enough bytes from bytestream!", ioEx); + } + } + + public static ZChunkHeaderLead readFileHeaderLead(final byte[] input) { + if (input.length < MAX_LEAD_SIZE) { + throw new IllegalArgumentException("No enough bytes to read lead."); + } + + final ZChunkLeadParser leadParser = ZChunkLeadParser.fromBytes(input); + + return getZChunkFileHeaderLeadFromParser(leadParser); + } + + public static ZChunkHeaderLead readFileHeaderLead(final File input) { + final ZChunkLeadParser leadParser = ZChunkLeadParser.fromFile(input); + + return getZChunkFileHeaderLeadFromParser(leadParser); + } + + private static ZChunkHeaderLead getZChunkFileHeaderLeadFromParser(final ZChunkLeadParser leadParser) { + return ImmutableZChunkHeaderLead.builder() + .id(leadParser.readLeadId()) + .checksumTypeInt(leadParser.readLeadCksumType()) + .headerSize(leadParser.readHeaderSize()) + .checksum(leadParser.readHeaderChecksum()) + .build(); + } + + private static byte[] readCompleteHeader(final File input, final ZChunkHeaderLead lead) { + try (final FileInputStream fis = new FileInputStream(input)) { + final int totalHeaderSize = OffsetUtil.getTotalHeaderSize(lead); + final byte[] buffer = new byte[totalHeaderSize]; + final int readCount = fis.read(buffer); + + if (readCount < totalHeaderSize) { + throw new IllegalArgumentException("Cannot read header, file too short?"); + } + + return buffer; + } catch (final IOException ioEx) { + throw new InvalidFileException("File too short?", input, ioEx); + } + } + + public static ZChunkHeaderPreface readHeaderPreface(final byte[] completeHeader, final ZChunkHeaderLead lead) { + final ZChunkPrefaceParser prefaceParser = ZChunkPrefaceParser.fromBytes(completeHeader, lead); + + return getZChunkFileHeaderPrefaceFromParser(prefaceParser); + } + + private static ZChunkHeaderPreface getZChunkFileHeaderPrefaceFromParser(final ZChunkPrefaceParser prefaceParser) { + final CompressedInt prefaceFlagsInt = prefaceParser.readFlagsInt(); + final Set flags = PrefaceFlag.getPrefaceFlags(prefaceFlagsInt); + + final CompressionAlgorithm compressionAlgorithm = ImmutableCompressionAlgorithm.builder() + .compressionTypeValue(prefaceParser.readCompressionType()) + .name("unknown") + .outputStreamSupplier(a -> a) + .build(); + + return ImmutableZChunkHeaderPreface.builder() + .totalDataChecksum(prefaceParser.readTotalDataCksum()) + .prefaceFlagsInt(prefaceFlagsInt) + .compressionAlgorithm(compressionAlgorithm) + .optionalElementCount(CompressedIntFactory.valueOf(0)) + .addAllPrefaceFlags(flags) + .build(); + } + + public static ZChunkHeaderPreface readFileHeaderPreface(final File input, final ZChunkHeaderLead lead) { + return readFileHeaderPreface(input, lead.getChecksumType(), OffsetUtil.getLeadLength(lead)); + } + + public static ZChunkHeaderPreface readFileHeaderPreface(final File zckFile, final HeaderChecksumType headerChecksumType, + final long leadLength) { + final byte[] cksum = new byte[headerChecksumType.getDigestLength()]; + + try (final RandomAccessFile randomAccessFile = new RandomAccessFile(zckFile, "r")) { + randomAccessFile.seek(leadLength); + randomAccessFile.read(cksum); + + final InputStream inputStream = Channels.newInputStream(randomAccessFile.getChannel()); + final CompressedInt flags = getPrefaceFlagsFromInputStream(inputStream); + + final CompressedInt compressionType = CompressedIntFactory.readCompressedInt(inputStream); + final CompressionAlgorithm compressionAlgorithm = CompressionAlgorithmFactory.forType(compressionType.getLongValue()); + + return ImmutableZChunkHeaderPreface.builder() + .totalDataChecksum(cksum) + .prefaceFlagsInt(flags) + .compressionAlgorithm(compressionAlgorithm) + .optionalElementCount(CompressedIntFactory.valueOf(0)) + .addAllPrefaceFlags(PrefaceFlag.getPrefaceFlags(flags)) + .build(); + } catch (final IOException ioEx) { + throw new InvalidFileException("Unable to read preface of file.", zckFile, ioEx); + } + } + + public static CompressedInt getPrefaceFlagsFromInputStream(final InputStream inputStream) throws IOException { + return CompressedIntFactory.readCompressedInt(inputStream); + } + + private static ZChunkHeaderIndex readHeaderIndex(final byte[] completeHeader, final ZChunkHeaderLead lead, + final ZChunkHeaderPreface preface) { + final ZChunkIndexParser parser = ZChunkIndexParser.fromBytes(completeHeader, lead, preface); + + final CompressedInt indexChecksumType = parser.readIndexCksumType(); + + return ImmutableZChunkHeaderIndex.builder() + .indexSize(parser.readIndexSize()) + .chunkChecksumTypeInt(indexChecksumType) + .chunkCount(parser.readChunkCount()) + .dictChecksum(parser.readDictChecksum()) + .dictLength(parser.readDictLength()) + .uncompressedDictLength(parser.readUncompressedDictLength()) + .addAllChunkInfo(parser.readChunkInfos()) + .build(); + } + + private static ZChunkHeaderSignatures readSignatureIndex( + final byte[] completeHeader, final ZChunkHeaderLead lead, final ZChunkHeaderPreface preface, final ZChunkHeaderIndex index) { + + return ImmutableZChunkHeaderSignatures.builder() + .signatureCount(CompressedIntFactory.valueOf(0L)) + .build(); + } + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderIndex.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderIndex.java new file mode 100644 index 0000000..0e7fff5 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderIndex.java @@ -0,0 +1,98 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.fileformat.util.ByteUtils; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; +import org.immutables.value.Value; + +/** + *
+ * +=================+==========================+==================+
+ * | Index size (ci) | Chunk checksum type (ci) | Chunk count (ci) |
+ * +=================+==========================+==================+
+ *
+ * (Dict stream will only exist if flag 0 is set to 1)
+ * +==================+===============+==================+
+ * | Dict stream (ci) | Dict checksum | Dict length (ci) |
+ * +==================+===============+==================+
+ *
+ * +===============================+
+ * | Uncompressed dict length (ci) |
+ * +===============================+
+ *
+ * (Chunk stream will only exist if flag 0 is set to 1)
+ * [+===================+================+===================+
+ * [| Chunk stream (ci) | Chunk checksum | Chunk length (ci) |
+ * [+===================+================+===================+
+ *
+ * +==========================+]
+ * | Uncompressed length (ci) |] ...
+ * +==========================+]
+ * 
+ */ +@Value.Immutable +public abstract class ZChunkHeaderIndex { + + public abstract CompressedInt getIndexSize(); + + public abstract CompressedInt getChunkChecksumTypeInt(); + + @Value.Derived + public IndexChecksumType getChunkChecksumType() { + return IndexChecksumType.values()[getChunkChecksumTypeInt().getIntValue()]; + } + + public abstract CompressedInt getChunkCount(); + + public abstract Optional getDictStream(); + + /** + * Must be all zeros if no dict present. + * + * @return dict checksum or all zeros. + */ + public abstract byte[] getDictChecksum(); + + /** + * Dict length or zero. + * + * @return dict length. + */ + public abstract CompressedInt getDictLength(); + + public abstract CompressedInt getUncompressedDictLength(); + + public abstract List getChunkInfo(); + + @Override + public String toString() { + return new StringJoiner(", ", ZChunkHeaderIndex.class.getSimpleName() + "[", "]") + .add("indexSize=" + getIndexSize()) + .add("chunkChecksumType=" + getChunkChecksumType()) + .add("chunkCount=" + getChunkCount()) + .add("dictStream=" + ByteUtils.byteArrayToHexString(getDictStream().orElse(new byte[0]))) + .add("dictChecksum=" + ByteUtils.byteArrayToHexString(getDictChecksum())) + .add("dictLength=" + getDictLength()) + .add("dictUncompressedLength=" + getUncompressedDictLength()) + .add("chunkInfo=" + getChunkInfo()) + .toString(); + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderLead.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderLead.java new file mode 100644 index 0000000..96a8621 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderLead.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.fileformat.err.InvalidFileException; +import de.bmarwell.zchunk.fileformat.util.ByteUtils; +import java.util.Arrays; +import java.util.StringJoiner; +import org.immutables.value.Value; + +/** + * The lead:
+ * + *

+ * +-+-+-+-+-+====================+==================+=================+
+ * | ID | Checksum type (ci) | Header size (ci) | Header checksum |
+ * +-+-+-+-+-+====================+==================+=================+ + *

+ */ +@Value.Immutable +public abstract class ZChunkHeaderLead { + + public abstract byte[] getId(); + + public abstract CompressedInt getChecksumTypeInt(); + + @Value.Derived + public HeaderChecksumType getChecksumType() { + return HeaderChecksumType.values()[getChecksumTypeInt().getIntValue()]; + } + + /** + * Header size: + * This is an integer containing the size of the header, not including the lead. + * + * @return Remaining bytes after the header. + */ + public abstract CompressedInt getHeaderSize(); + + /** + * Checksum of the whole header. + * + * @return the checksum of the whole header. + */ + public abstract byte[] getChecksum(); + + @Value.Check + public void checkLead() { + if (!Arrays.equals(ZChunkConstants.Header.FILE_MAGIC, this.getId())) { + throw new InvalidFileException("file magic differs: [" + ByteUtils.byteArrayToHexString(this.getId()) + "]."); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", ZChunkHeaderLead.class.getSimpleName() + "[", "]") + .add("id='" + ByteUtils.byteArrayToHexString(getId()) + "'") + .add("cksumtype=" + getChecksumTypeInt().getValue().toString()) + .add("cksumtype='" + getChecksumType() + "'") + .add("headerSize=" + getHeaderSize().getValue().toString()) + .add("cksum='" + ByteUtils.byteArrayToHexString(getChecksum()) + "'") + .toString(); + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderPreface.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderPreface.java new file mode 100644 index 0000000..b337753 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderPreface.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.compression.api.CompressionAlgorithm; +import de.bmarwell.zchunk.fileformat.util.ByteUtils; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; +import org.immutables.value.Value; + +/** + *

+ * +===============+============+========================+
+ * | Data checksum | Flags (ci) | Compression type (ci ) |
+ * +===============+============+========================+
+ *
+ * (Optional elements will only be set if flag 1 is set to 1)
+ * + * +=============================+
+ * | Optional element count (ci) |
+ * +=============================+
+ *
+ * [+==========================+=================================+
+ * [| Optional element id (ci) | Optional element data size (ci) |
+ * [+==========================+=================================+
+ *
+ * +=======================+]
+ * | Optional element data |] ...
+ * +=======================+]
+ *

+ */ +@Value.Immutable +public abstract class ZChunkHeaderPreface { + + /** + * Returns the checksum of the data segment. + * + * @return the checksum of the data segment. + */ + public abstract byte[] getTotalDataChecksum(); + + public abstract CompressedInt getPrefaceFlagsInt(); + + public abstract Set getPrefaceFlags(); + + @Value.Derived + public boolean hasOptionalElements() { + return getPrefaceFlags().contains(PrefaceFlag.HAS_OPTIONAL_ELEMENTS); + } + + public abstract CompressionAlgorithm getCompressionAlgorithm(); + + public abstract CompressedInt getOptionalElementCount(); + + public abstract List getOptionalElements(); + + /** + * Flags + * This is a compressed integer containing a bitmask of the flags. All unused + * flags MUST be set to 0. If a decoder sees a flag set that it doesn't + * recognize, it MUST exit with an error. + */ + @Value.Check + public void testUnknownFlags() { + final Set flags = getPrefaceFlags(); + + if (flags.contains(PrefaceFlag.HAS_DATA_STREAMS)) { + throw new UnsupportedOperationException("Not implemented: " + PrefaceFlag.HAS_DATA_STREAMS); + } + + if (flags.contains(PrefaceFlag.HAS_OPTIONAL_ELEMENTS)) { + throw new UnsupportedOperationException("Not implemented: " + PrefaceFlag.HAS_OPTIONAL_ELEMENTS); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", ZChunkHeaderPreface.class.getSimpleName() + "[", "]") + .add("cksum='" + ByteUtils.byteArrayToHexString(getTotalDataChecksum()) + "'") + .add("flags='" + getPrefaceFlags() + "'") + .add("compressionAlgorithm=" + getCompressionAlgorithm()) + .add("optionalElementCount=" + getOptionalElementCount()) + .add("optionalElements=" + getOptionalElements()) + .toString(); + } + + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderSignatures.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderSignatures.java new file mode 100644 index 0000000..16b06fd --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderSignatures.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import org.immutables.value.Value; + +@Value.Immutable +public abstract class ZChunkHeaderSignatures { + + public abstract CompressedInt getSignatureCount(); + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/err/InvalidFileException.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/err/InvalidFileException.java new file mode 100644 index 0000000..81e7f2b --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/err/InvalidFileException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.err; + +import java.io.File; +import java.util.Optional; +import java.util.StringJoiner; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class InvalidFileException extends RuntimeException { + + private final @Nullable File sourceFile; + + public InvalidFileException(final String message) { + super(message); + this.sourceFile = null; + } + + public InvalidFileException(final String message, final File zckFile) { + super(message); + this.sourceFile = zckFile; + } + + public InvalidFileException(final String message, final File zckFile, final Throwable cause) { + super(message, cause); + this.sourceFile = zckFile; + } + + public Optional getSourceFile() { + return Optional.ofNullable(this.sourceFile); + } + + @Override + public String toString() { + return new StringJoiner(", ", InvalidFileException.class.getSimpleName() + "[", "]") + .add("super=" + super.toString()) + .add("sourceFile='" + getSourceFile().map(File::getAbsolutePath).orElse("unknown") + "'") + .toString(); + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/package-info.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/package-info.java new file mode 100644 index 0000000..7ecc485 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@Value.Style(stagedBuilder = true, jdkOnly = true) +package de.bmarwell.zchunk.fileformat; + +import org.immutables.value.Value; 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 new file mode 100644 index 0000000..ea84f89 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkIndexParser.java @@ -0,0 +1,224 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.parser; + +import static java.util.Collections.emptyList; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.compressedint.CompressedIntFactory; +import de.bmarwell.zchunk.fileformat.ImmutableZChunkHeaderChunkInfo; +import de.bmarwell.zchunk.fileformat.IndexChecksumType; +import de.bmarwell.zchunk.fileformat.PrefaceFlag; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderChunkInfo; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderLead; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderPreface; +import de.bmarwell.zchunk.fileformat.util.OffsetUtil; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +public final class ZChunkIndexParser { + + private final byte[] completeHeader; + private final ZChunkHeaderLead lead; + private final ZChunkHeaderPreface preface; + private final long indexStart; + private long cksumTypeOffset = -1; + private long chunkCountOffset = -1; + private long optionalDictStreamOffset = -1L; + private long dictChecksumOffest = -1L; + private long dictLengthOffset = -1L; + private long dictUncompressedLengthOffset = -1L; + private long chunkStreamOffset = -1L; + private CompressedInt chunkCount; + private IndexChecksumType chunkChecksumType; + + private ZChunkIndexParser(final byte[] completeHeader, final ZChunkHeaderLead lead, final ZChunkHeaderPreface preface) { + this.completeHeader = completeHeader; + this.lead = lead; + this.preface = preface; + this.indexStart = OffsetUtil.getLeadLength(lead) + OffsetUtil.getPrefaceLength(preface); + } + + public static ZChunkIndexParser fromBytes(final byte[] completeHeader, final ZChunkHeaderLead lead, final ZChunkHeaderPreface preface) { + return new ZChunkIndexParser(completeHeader, lead, preface); + } + + public CompressedInt readIndexSize() { + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.completeHeader)) { + bis.skip(this.indexStart); + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + this.cksumTypeOffset = this.indexStart + compressedInt.getCompressedBytes().length; + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.indexStart + "]!", ioEx); + } + } + + public CompressedInt readIndexCksumType() { + if (this.cksumTypeOffset == -1L) { + readIndexSize(); + } + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.completeHeader)) { + bis.skip(this.cksumTypeOffset); + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + this.chunkCountOffset = this.cksumTypeOffset + compressedInt.getCompressedBytes().length; + this.chunkChecksumType = IndexChecksumType.values()[compressedInt.getIntValue()]; + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.cksumTypeOffset + "]!", ioEx); + } + } + + public CompressedInt readChunkCount() { + if (this.chunkCountOffset == -1) { + readIndexCksumType(); + } + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.completeHeader)) { + bis.skip(this.chunkCountOffset); + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + this.optionalDictStreamOffset = this.chunkCountOffset + compressedInt.getCompressedBytes().length; + this.chunkCount = compressedInt; + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.chunkCountOffset + "]!", ioEx); + } + } + + public byte[] readDictStream() { + if (this.optionalDictStreamOffset == -1L) { + readChunkCount(); + } + + if (!this.preface.getPrefaceFlags().contains(PrefaceFlag.HAS_DATA_STREAMS)) { + this.dictChecksumOffest = this.optionalDictStreamOffset; + return new byte[0]; + } + + // TODO: implement property. + this.dictChecksumOffest = this.optionalDictStreamOffset; + return new byte[0]; + } + + public byte[] readDictChecksum() { + if (this.dictChecksumOffest == -1L) { + 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(); + final byte[] dictChecksum = new byte[dictChecksumLength]; + bis.read(dictChecksum); + + this.dictLengthOffset = this.dictChecksumOffest + dictChecksumLength; + + return dictChecksum; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read byte[] int at offset [" + this.dictChecksumOffest + "]!", ioEx); + } + } + + public CompressedInt readDictLength() { + if (this.dictLengthOffset == -1L) { + readDictChecksum(); + } + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.completeHeader)) { + bis.skip(this.dictLengthOffset); + + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + this.dictUncompressedLengthOffset = this.dictLengthOffset + compressedInt.getCompressedBytes().length; + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.dictLengthOffset + "]!", ioEx); + } + } + + public CompressedInt readUncompressedDictLength() { + if (this.dictUncompressedLengthOffset == -1L) { + readDictChecksum(); + } + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.completeHeader)) { + bis.skip(this.dictUncompressedLengthOffset); + + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + this.chunkStreamOffset = this.dictUncompressedLengthOffset + compressedInt.getCompressedBytes().length; + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.dictLengthOffset + "]!", ioEx); + } + } + + public Iterable readChunkInfos() { + if (this.chunkStreamOffset == -1L) { + readUncompressedDictLength(); + } + + if (this.chunkCount.getValue().equals(BigInteger.ZERO)) { + return emptyList(); + } + + final List chunkInfo = new ArrayList<>(); + + long currentOffset = 0; + // first chunk is the dict chunk. + for (long chunkNumber = 0; chunkNumber < this.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)) { + //TODO: chunkStream + throw new UnsupportedOperationException("data streams not implemented."); + } + + final byte[] chunkChecksum = new byte[this.chunkChecksumType.actualChecksumLength()]; + bis.read(chunkChecksum); + + final CompressedInt compressedChunkLength = CompressedIntFactory.readCompressedInt(bis); + final CompressedInt uncompressedChunkLength = CompressedIntFactory.readCompressedInt(bis); + + chunkInfo.add(ImmutableZChunkHeaderChunkInfo.builder() + .currentIndex(chunkNumber) + .chunkChecksum(chunkChecksum) + .chunkLength(compressedChunkLength) + .chunkUncompressedLength(uncompressedChunkLength) + .build()); + + currentOffset += this.chunkChecksumType.actualChecksumLength() + + compressedChunkLength.getCompressedBytes().length + + uncompressedChunkLength.getCompressedBytes().length; + } catch (final IOException ioEx) { + throw new IllegalArgumentException( + "Cannot read chunk info no. [" + chunkNumber + "] at offset [" + this.chunkStreamOffset + "]!", ioEx); + } + } + + return chunkInfo; + } +} 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 new file mode 100644 index 0000000..3dfe80e --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkLeadParser.java @@ -0,0 +1,154 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.parser; + +import static de.bmarwell.zchunk.fileformat.ZChunkConstants.Header.FILE_MAGIC; +import static de.bmarwell.zchunk.fileformat.ZChunkConstants.Header.MAX_LEAD_SIZE; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.compressedint.CompressedIntFactory; +import de.bmarwell.zchunk.fileformat.HeaderChecksumType; +import de.bmarwell.zchunk.fileformat.err.InvalidFileException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public class ZChunkLeadParser { + + private final byte[] leadBytes = new byte[MAX_LEAD_SIZE]; + private HeaderChecksumType checksumType; + private long headerSizeOffset; + private long headerChecksumOffset; + + /** + * Private constructor because of possible unsafe operations. + * + * @param leadBytes + * the bytes belonging to the lead. + */ + private ZChunkLeadParser(final byte[] leadBytes) { + if (leadBytes.length < MAX_LEAD_SIZE) { + throw new IllegalArgumentException(""); + } + + System.arraycopy(leadBytes, 0, this.leadBytes, 0, MAX_LEAD_SIZE); + } + + /** + * Create a parser from the given bytes. + * + * @param leadBytes + * the bytes to take as a lead. + * @return a parser instance. + */ + public static ZChunkLeadParser fromBytes(final byte[] leadBytes) { + return new ZChunkLeadParser(leadBytes); + } + + public static ZChunkLeadParser fromFile(final File input) { + try (final FileInputStream fr = new FileInputStream(input)) { + final byte[] buffer = new byte[MAX_LEAD_SIZE]; + final int read = fr.read(buffer); + + if (read < MAX_LEAD_SIZE) { + throw new InvalidFileException(getExceptionMessage(input), input); + } + + if (read != buffer.length) { + throw new InvalidFileException(getExceptionMessage(input), input); + } + + return new ZChunkLeadParser(buffer); + } catch (final IOException ioEx) { + throw new InvalidFileException(getExceptionMessage(input), input, ioEx); + } + } + + private static String getExceptionMessage(final File input) { + return String.format("Unable to read [%d] bytes from file [%s].", MAX_LEAD_SIZE, input.getAbsolutePath()); + } + + public byte[] readLeadId() { + final int length = FILE_MAGIC.length; + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.leadBytes)) { + final byte[] leadid = new byte[length]; + bis.read(leadid); + + return leadid; + } catch (final IOException e) { + throw new IllegalArgumentException("Not a zchunk lead."); + } + } + + public CompressedInt readLeadCksumType() { + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.leadBytes)) { + final long skip = bis.skip(FILE_MAGIC.length); + if (skip < FILE_MAGIC.length) { + throw new IllegalStateException("Unable to skip [" + FILE_MAGIC.length + "] bytes!"); + } + + final CompressedInt checksumType = CompressedIntFactory.readCompressedInt(bis); + this.headerSizeOffset = FILE_MAGIC.length + (long) checksumType.getCompressedBytes().length; + this.checksumType = HeaderChecksumType.values()[checksumType.getIntValue()]; + + return checksumType; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + FILE_MAGIC.length + "]!", ioEx); + } + } + + public CompressedInt readHeaderSize() { + if (this.headerSizeOffset == -1L) { + readLeadCksumType(); + } + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.leadBytes)) { + bis.skip(this.headerSizeOffset); + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + this.headerChecksumOffset = this.headerSizeOffset + compressedInt.getCompressedBytes().length; + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.headerSizeOffset + "]!", ioEx); + } + } + + public byte[] readHeaderChecksum() { + if (this.headerChecksumOffset == -1L) { + readHeaderSize(); + } + + final int cksumLength = this.checksumType.getDigestLength(); + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.leadBytes)) { + bis.skip(this.headerChecksumOffset); + final byte[] buffer = new byte[cksumLength]; + final int readBytes = bis.read(buffer); + + if (readBytes < cksumLength) { + throw new IllegalStateException("Unable to read [" + cksumLength + "] bytes for checksum!"); + } + + return buffer; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read cksum of length [" + cksumLength + "] at offset [" + this.headerChecksumOffset + "]!", + ioEx); + } + } +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkPrefaceParser.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkPrefaceParser.java new file mode 100644 index 0000000..b083f97 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/parser/ZChunkPrefaceParser.java @@ -0,0 +1,95 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.parser; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import de.bmarwell.zchunk.compressedint.CompressedIntFactory; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderLead; +import de.bmarwell.zchunk.fileformat.util.OffsetUtil; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class ZChunkPrefaceParser { + + private final byte[] header; + private final ZChunkHeaderLead lead; + private final long flagsOffset; + private final long prefaceOffset; + private long compressionTypeOffset = -1L; + + private ZChunkPrefaceParser(final byte[] completeHeader, final ZChunkHeaderLead lead) { + this.header = completeHeader; + this.lead = lead; + this.prefaceOffset = OffsetUtil.getLeadLength(lead); + this.flagsOffset = this.prefaceOffset + lead.getChecksumType().getDigestLength(); + } + + public static ZChunkPrefaceParser fromBytes(final byte[] completeHeader, final ZChunkHeaderLead lead) { + if (completeHeader.length < OffsetUtil.getTotalHeaderSize(lead)) { + throw new IllegalArgumentException("Byte array to short!"); + } + + return new ZChunkPrefaceParser(completeHeader, lead); + } + + public byte[] readTotalDataCksum() { + final int digestLength = this.lead.getChecksumType().getDigestLength(); + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.header)) { + final long skip = bis.skip(this.prefaceOffset); + final byte[] cksum = new byte[digestLength]; + final int read = bis.read(cksum, 0, digestLength); + + if (read < digestLength) { + throw new IllegalArgumentException("Cannot read cksum of length [" + digestLength + "] at offset [" + this.prefaceOffset + "]!"); + } + + return cksum; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read cksum of length [" + digestLength + "] at offset [" + this.prefaceOffset + "]!", + ioEx); + } + } + + public CompressedInt readFlagsInt() { + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.header)) { + bis.skip(this.flagsOffset); + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + this.compressionTypeOffset = this.flagsOffset + compressedInt.getCompressedBytes().length; + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.flagsOffset + "]!", ioEx); + } + } + + public CompressedInt readCompressionType() { + if (this.compressionTypeOffset == -1L) { + readFlagsInt(); + } + + try (final ByteArrayInputStream bis = new ByteArrayInputStream(this.header)) { + bis.skip(this.compressionTypeOffset); + final CompressedInt compressedInt = CompressedIntFactory.readCompressedInt(bis); + + return compressedInt; + } catch (final IOException ioEx) { + throw new IllegalArgumentException("Cannot read compressed int at offset [" + this.compressionTypeOffset + "]!", ioEx); + } + } + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/ByteUtils.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/ByteUtils.java new file mode 100644 index 0000000..ad51669 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/ByteUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.util; + +import de.bmarwell.zchunk.compressedint.CompressedIntUtil; +import java.math.BigInteger; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public final class ByteUtils { + + private ByteUtils() { + // util class + } + + public static String byteArrayToHexString(final byte[] input) { + if (input.length == 0) { + return ""; + } + + final String hexString = new BigInteger(1, input).toString(16); + + if (hexString.length() < input.length * 2) { + final int missing = input.length * 2 - hexString.length(); + final String zeros = new String(new char[missing]).replace("\0", "0"); + + return zeros + hexString; + } + + return hexString; + } + + public static String byteArrayToBinaryString(final byte[] input) { + return new BigInteger(1, input).toString(2); + } + + public static String longToBinaryString(final long input) { + return BigInteger.valueOf(input).toString(2); + } + + public static byte[] hexStringToByteArray(final String inputString) { + final String cleanString = inputString.replaceAll(" ", "").toLowerCase(Locale.ENGLISH); + final int len = cleanString.length(); + + if ((len % 2) != 0) { + throw new IllegalArgumentException("not a valid hex string: [" + cleanString + "]."); + } + + if (!cleanString.matches("^([0-9a-f][0-9a-f])+$")) { + throw new IllegalArgumentException("hex string does not consist of only hex chars: [" + cleanString + "]."); + } + + final byte[] data = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + final int i1 = Character.digit(cleanString.charAt(i), 16) << 4; + final int i2 = Character.digit(cleanString.charAt(i + 1), 16); + + data[i / 2] = (byte) (i1 + i2); + } + + return data; + } + + public static boolean decrease(final AtomicReference remainingFlagLong, final long bitflag) { + final AtomicBoolean changed = new AtomicBoolean(); + remainingFlagLong.getAndUpdate(curr -> getLongUnaryOperator(curr, bitflag, changed)); + + return changed.get(); + } + + public static BigInteger getLongUnaryOperator(final BigInteger curr, final long bitflag, final AtomicBoolean changed) { + // todo: check bigint is not bigger than (long.max * 2) + 1; + final long longValue = curr.longValue(); + + if ((longValue & bitflag) == bitflag) { + changed.set(true); + return BigInteger.valueOf(longValue & ~bitflag).and(CompressedIntUtil.UNSIGNED_LONG_MASK); + } + + return BigInteger.valueOf(longValue).and(CompressedIntUtil.UNSIGNED_LONG_MASK); + } + + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/ChecksumUtil.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/ChecksumUtil.java new file mode 100644 index 0000000..c42ac63 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/ChecksumUtil.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.util; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; + +import de.bmarwell.zchunk.fileformat.HeaderChecksumType; +import de.bmarwell.zchunk.fileformat.ZChunkHeader; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderChunkInfo; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderIndex; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderLead; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderPreface; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderSignatures; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +public final class ChecksumUtil { + + private static final Logger LOG = Logger.getLogger(ChecksumUtil.class.getCanonicalName()); + + private ChecksumUtil() { + // util + } + + public static boolean isValid(final ZChunkHeader header) { + final byte[] expectedChecksum = header.getLead().getChecksum(); + final byte[] calculatedChecksum = calculateHeaderChecksum(header); + + return Arrays.equals(expectedChecksum, calculatedChecksum); + } + + public static byte[] calculateHeaderChecksum(final ZChunkHeader header) { + final HeaderChecksumType digestAlgorithm = header.getLead().getChecksumType(); + final MessageDigest digest = digestAlgorithm.digest(); + + digest.update(getLeadBytes(header.getLead())); + digest.update(getPrefaceBytes(header.getPreface())); + digest.update(getIndexBytes(header.getIndex())); + digest.update(getSignatureBytes(header.getSignatures())); + + return digest.digest(); + } + + public static byte[] getHeaderWithoutChecksum(final ZChunkHeader header) { + final ZChunkHeaderLead lead = header.getLead(); + final byte[] leadBytes = getLeadBytes(lead); + + final ZChunkHeaderPreface preface = header.getPreface(); + final byte[] prefaceBytes = getPrefaceBytes(preface); + + final ZChunkHeaderIndex index = header.getIndex(); + final byte[] indexBytes = getIndexBytes(index); + + final byte[] sigs = getSignatureBytes(header.getSignatures()); + + return concat(leadBytes, prefaceBytes, indexBytes, sigs); + } + + private static byte[] getSignatureBytes(final ZChunkHeaderSignatures signatures) { + return concat( + signatures.getSignatureCount().getCompressedBytes() + ); + } + + private static byte[] getIndexBytes(final ZChunkHeaderIndex index) { + final byte[] chunkInfos = getChunkInfoBytes(index.getChunkInfo()); + + final byte[] indexBytes = concat( + index.getIndexSize().getCompressedBytes(), + index.getChunkChecksumTypeInt().getCompressedBytes(), + index.getChunkCount().getCompressedBytes(), + index.getDictStream().orElse(new byte[0]), + index.getDictChecksum(), + index.getDictLength().getCompressedBytes(), + index.getUncompressedDictLength().getCompressedBytes(), + chunkInfos + ); + + return indexBytes; + } + + private static byte[] getChunkInfoBytes(final List chunkInfos) { + final List collect = chunkInfos.stream() + .map(ChecksumUtil::getChunkInfoBytes) + .collect(toList()); + + return concatByteArrays(collect); + } + + private static byte[] getChunkInfoBytes(final ZChunkHeaderChunkInfo info) { + return concat( + info.getChunkStream().orElse(new byte[0]), + info.getChunkChecksum(), + info.getChunkLength().getCompressedBytes(), + info.getChunkUncompressedLength().getCompressedBytes() + ); + } + + + private static byte[] getPrefaceBytes(final ZChunkHeaderPreface preface) { + final byte[] prefaceBytes = concat( + preface.getTotalDataChecksum(), + preface.getPrefaceFlagsInt().getCompressedBytes(), + preface.getCompressionAlgorithm().getCompressionTypeValue().getCompressedBytes() + ); + + if (preface.hasOptionalElements()) { + final byte[] prefaceWithOptional = concat( + prefaceBytes, + preface.getOptionalElementCount().getCompressedBytes() + // TODO: optional elements header + ); + + return prefaceWithOptional; + } + + return prefaceBytes; + } + + private static byte[] getLeadBytes(final ZChunkHeaderLead lead) { + final byte[] leadBytes = concat( + lead.getId(), + lead.getChecksumTypeInt().getCompressedBytes(), + lead.getHeaderSize().getCompressedBytes() + ); + + return leadBytes; + } + + private static byte[] concat(final byte[]... bytes) { + final List bytes1 = asList(bytes); + + return concatByteArrays(bytes1); + } + + private static byte[] concatByteArrays(final List byteList) { + final int totalLength = byteList.stream() + .mapToInt(by -> by.length) + .sum(); + + final AtomicInteger targetOffset = new AtomicInteger(); + final byte[] target = new byte[totalLength]; + + byteList.forEach(by -> targetOffset.getAndUpdate(off -> { + System.arraycopy(by, 0, target, off, by.length); + return off + by.length; + })); + return target; + } + + +} diff --git a/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/OffsetUtil.java b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/OffsetUtil.java new file mode 100644 index 0000000..0afe766 --- /dev/null +++ b/fileformat/src/main/java/de/bmarwell/zchunk/fileformat/util/OffsetUtil.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.util; + +import de.bmarwell.zchunk.fileformat.OptionalElement; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderLead; +import de.bmarwell.zchunk.fileformat.ZChunkHeaderPreface; +import java.math.BigInteger; + +public final class OffsetUtil { + + private OffsetUtil() { + // util. + } + + /** + * The length of the lead. After the lead, the prefix will begin (2nd part of the header). + * + * @return the lead length (id/magic length + checksum ci length + headersize ci length + cksum length). + */ + public static int getLeadLength(final ZChunkHeaderLead lead) { + return BigInteger.valueOf(lead.getId().length) + .add(BigInteger.valueOf(lead.getChecksumTypeInt().getCompressedBytes().length)) + .add(BigInteger.valueOf(lead.getHeaderSize().getCompressedBytes().length)) + .add(BigInteger.valueOf(lead.getChecksum().length)) + .intValueExact(); + } + + public static int getTotalHeaderSize(final ZChunkHeaderLead lead) { + return getLeadLength(lead) + lead.getHeaderSize().getIntValue(); + } + + public static long getPrefaceLength(final ZChunkHeaderPreface preface) { + /* + * optional element count only exists if the flag is set. + */ + final long optElementCountBytes = getOptElementCountBytes(preface); + + return BigInteger.valueOf(preface.getTotalDataChecksum().length) + // plus highest preface flag + .add(BigInteger.valueOf(preface.getPrefaceFlagsInt().getCompressedBytes().length)) + // plus length of compression type + .add(BigInteger.valueOf(preface.getCompressionAlgorithm().getCompressionTypeValue().getCompressedBytes().length)) + // this might even be 0 if the flag was not set. + .add(BigInteger.valueOf(optElementCountBytes)) + .add(BigInteger.valueOf( + preface.getOptionalElements().stream() + .mapToLong(OptionalElement::getTotalLength) + .sum() + )) + .longValueExact(); + } + + private static long getOptElementCountBytes(final ZChunkHeaderPreface preface) { + if (preface.getOptionalElementCount().getLongValue() == 0L) { + return 0L; + } + + return preface.getOptionalElementCount().getCompressedBytes().length; + } + +} diff --git a/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/ZChunkFileTest.java b/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/ZChunkFileTest.java new file mode 100644 index 0000000..1b0bc80 --- /dev/null +++ b/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/ZChunkFileTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.fileformat.util.ByteUtils; +import de.bmarwell.zchunk.fileformat.util.ChecksumUtil; +import de.bmarwell.zchunk.fileformat.util.OffsetUtil; +import java.io.File; +import java.util.logging.Logger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("SpellCheckingInspection") +public class ZChunkFileTest { + + private static final Logger LOG = Logger.getLogger(ZChunkFileTest.class.getCanonicalName()); + + private static final File TEST_FILE = new File(ZChunkFileTest.class.getResource("/testfiles/LICENSE.dict.fodt.zck").getPath()); + private static final File TEST_FILE_INVALID = new File( + ZChunkFileTest.class.getResource("/testfiles/LICENSE.dict.fodt.zck.invalid").getPath()); + + @Test + public void testFileFormat() { + final ZChunkFile zChunkFile = ZChunkHeaderFactory.fromFile(TEST_FILE); + + final ZChunkHeader header = zChunkFile.getHeader(); + + Assertions.assertAll( + () -> testLead(header.getLead()), + () -> testPreface(header.getPreface()), + () -> testIndex(header.getIndex()), + () -> checkChecksum(header) + ); + } + + private void checkChecksum(final ZChunkHeader header) { + final byte[] exp = header.getLead().getChecksum(); + final byte[] actual = ChecksumUtil.calculateHeaderChecksum(header); + + Assertions.assertAll( + () -> Assertions.assertArrayEquals(exp, actual), + () -> Assertions.assertTrue(ChecksumUtil.isValid(header)) + ); + } + + @Test + public void testInvalidFile() { + Assertions.assertAll( + () -> Assertions.assertThrows(IllegalArgumentException.class, () -> ZChunkHeaderFactory.readFileHeaderLead(TEST_FILE_INVALID)), + () -> Assertions.assertThrows(IllegalArgumentException.class, () -> ZChunkHeaderFactory.fromFile(TEST_FILE_INVALID)) + ); + } + + private void testLead(final ZChunkHeaderLead lead) { + final byte[] expectedCksum = ByteUtils.hexStringToByteArray("f666ca3e0330c42aa4bfbb58ab0576788d720cbbb1af0291f11e256b7bda2e79"); + + Assertions.assertAll( + () -> Assertions.assertArrayEquals(ZChunkConstants.Header.FILE_MAGIC, lead.getId()), + () -> Assertions.assertEquals(1, lead.getChecksumType().ordinal()), + () -> Assertions.assertEquals(394L, lead.getHeaderSize().getLongValue()), + () -> Assertions.assertEquals(394, lead.getHeaderSize().getIntValue()), + // TODO: 434 (reported by zck_read_header) vs 432 (this implementation). + () -> Assertions.assertEquals(434L, OffsetUtil.getTotalHeaderSize(lead)), + () -> Assertions.assertArrayEquals(expectedCksum, lead.getChecksum()) + ); + } + + private void testPreface(final ZChunkHeaderPreface preface) { + final byte[] expectedDataCksum = ByteUtils.hexStringToByteArray("772aa76adc41e290dadff603b761ba02faad4681df05030539ae0ecd925ccd05"); + + Assertions.assertAll( + () -> Assertions.assertArrayEquals(expectedDataCksum, preface.getTotalDataChecksum()), + () -> Assertions.assertTrue(preface.getPrefaceFlags().isEmpty()), + () -> Assertions.assertEquals(2L, preface.getCompressionAlgorithm().getCompressionTypeValue().getLongValue()), + () -> Assertions.assertTrue(preface.getOptionalElements().isEmpty()) + ); + } + + private void testIndex(final ZChunkHeaderIndex index) { + Assertions.assertAll( + () -> Assertions.assertEquals(357L, index.getIndexSize().getLongValue()), + () -> Assertions.assertEquals(IndexChecksumType.SHA512_128, index.getChunkChecksumType()), + () -> Assertions.assertEquals(3, index.getChunkChecksumType().ordinal()), + () -> Assertions.assertEquals(17L, index.getChunkCount().getLongValue()) + ); + } + +} diff --git a/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactoryTest.java b/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactoryTest.java new file mode 100644 index 0000000..54ae762 --- /dev/null +++ b/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/ZChunkHeaderFactoryTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat; + +import de.bmarwell.zchunk.compressedint.CompressedInt; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ZChunkHeaderFactoryTest { + + @Test + public void testKnownPrefaceFlags() throws IOException { + final ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{0b00000001}); + final CompressedInt ci = ZChunkHeaderFactory.getPrefaceFlagsFromInputStream(in); + final Set prefaceFlags = PrefaceFlag.getPrefaceFlags(ci); + + Assertions.assertAll( + () -> Assertions.assertEquals(1, prefaceFlags.size()), + () -> Assertions.assertEquals(PrefaceFlag.HAS_DATA_STREAMS, prefaceFlags.iterator().next()) + ); + } + + +} diff --git a/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/util/ByteUtilTest.java b/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/util/ByteUtilTest.java new file mode 100644 index 0000000..cf7db1d --- /dev/null +++ b/fileformat/src/test/java/de/bmarwell/zchunk/fileformat/util/ByteUtilTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019, the zchunk-java contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.bmarwell.zchunk.fileformat.util; + +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ByteUtilTest { + + @Test + public void testDecrease() { + final BigInteger testLong = new BigInteger(1, new byte[]{0b00001000}); + + final AtomicReference reference = new AtomicReference<>(testLong); + final boolean decrease = ByteUtils.decrease(reference, (long) 0b00001000); + + Assertions.assertAll( + () -> Assertions.assertTrue(decrease), + () -> Assertions.assertEquals(0L, reference.get().longValueExact()) + ); + } + + @Test + public void testDecrease_other() { + final BigInteger testLong = new BigInteger(1, new byte[]{0b00001000}); + + final AtomicReference reference = new AtomicReference<>(testLong); + final boolean decrease = ByteUtils.decrease(reference, (long) 0b00000001); + + // nothing was decreased, the testlong should still have its old value. + Assertions.assertAll( + () -> Assertions.assertFalse(decrease), + () -> Assertions.assertEquals(testLong.longValueExact(), reference.get().longValueExact()) + ); + } +} diff --git a/fileformat/src/test/resources/logging.properties b/fileformat/src/test/resources/logging.properties new file mode 100644 index 0000000..7230d09 --- /dev/null +++ b/fileformat/src/test/resources/logging.properties @@ -0,0 +1,26 @@ +# +# Copyright 2019, the zchunk-java contributors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +handlers=java.util.logging.ConsoleHandler +# default log level +.level=FINE +# console handler settings +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=[%1$tFT%1$tT.%1$tLZ] [%4$-6s] %2$s - %5$s%6$s%n +# class specific settings +de.bmarwell.zchunk.fileformat.level=FINER +# 3rd party +org.junit.platform.level=INFO diff --git a/fileformat/src/test/resources/testfiles/LICENSE.dict.fodt.zck b/fileformat/src/test/resources/testfiles/LICENSE.dict.fodt.zck new file mode 100644 index 0000000..d82fc4c Binary files /dev/null and b/fileformat/src/test/resources/testfiles/LICENSE.dict.fodt.zck differ diff --git a/fileformat/src/test/resources/testfiles/LICENSE.dict.fodt.zck.invalid b/fileformat/src/test/resources/testfiles/LICENSE.dict.fodt.zck.invalid new file mode 100644 index 0000000..d1f328c Binary files /dev/null and b/fileformat/src/test/resources/testfiles/LICENSE.dict.fodt.zck.invalid differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..076c5f5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,114 @@ + + + + + 4.0.0 + + de.bmarwell.zchunk + zchunk-parent + 1.0.0-SNAPSHOT + + pom + + + compressedint + + compression/compression-api + + fileformat + + + + UTF-8 + + + 2.7.5 + + 5.4.0 + + + + + + + org.immutables + value + ${dependency.immutables.version} + provided + + + + + org.checkerframework + checker-qual + 2.8.1 + provided + + + + + + + org.junit.jupiter + junit-jupiter-api + ${dependency.junit-jupiter.version} + test + + + + org.junit.jupiter + junit-jupiter-engine + ${dependency.junit-jupiter.version} + test + + + + + + + + + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + 1.8 + + + + + + + maven-surefire-plugin + 3.0.0-M3 + + + ${project.build.testOutputDirectory}/logging.properties + EN + + + + + + + +