You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2022/12/10 15:55:59 UTC
[commons-compress] branch master updated: [COMPRESS-633] Add encryption support for SevenZ (#332)
This is an automated email from the ASF dual-hosted git repository.
ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git
The following commit(s) were added to refs/heads/master by this push:
new f0d13f96 [COMPRESS-633] Add encryption support for SevenZ (#332)
f0d13f96 is described below
commit f0d13f961fedfe93f3114778608edca8533cd717
Author: Daniel Santos <39...@users.noreply.github.com>
AuthorDate: Sun Dec 11 02:55:55 2022 +1100
[COMPRESS-633] Add encryption support for SevenZ (#332)
* feat: Encyrption support for Seven7
Implementation of password-based encryption for 7z compressor
COMPRESS-633
* feat: Encyrption support for Seven7 without `AES/CBC/PKCS5Padding`
As `AES/CBC/PKCS5Padding` is raised as weak of security, a manual implementation to fill cither block size is done
COMPRESS-633
* feat: Encyrption support for SevenZ
- implementation without storing password in a clear way
- several corrections suggeested by reviewers
COMPRESS-633
* feat: Encyrption support for SevenZ
typo
COMPRESS-633
* feat: Encyrption support for SevenZ
Avoid incrasing the public API surface with uneccessary method
COMPRESS-633
* feat: Encyrption support for SevenZ
no IDE specifi config files
COMPRESS-633
* Fix spelling
* Update super class from master
* AES256Options does not need to be public
* Fix spelling in Javadoc
Co-authored-by: Gary Gregory <ga...@users.noreply.github.com>
---
.gitignore | 1 +
.../compress/archivers/sevenz/AES256Options.java | 100 +++++++++++++
.../archivers/sevenz/AES256SHA256Decoder.java | 165 ++++++++++++++++++---
.../compress/archivers/sevenz/SevenZFile.java | 18 +--
.../archivers/sevenz/SevenZOutputFile.java | 78 ++++++++--
.../archivers/sevenz/SevenZOutputFileTest.java | 72 ++++++++-
6 files changed, 379 insertions(+), 55 deletions(-)
diff --git a/.gitignore b/.gitignore
index aae2af58..c57d236b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ target
.classpath
.settings
.idea
+.vscode
*.iml
*~
/.externalToolBuilders/
diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256Options.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256Options.java
new file mode 100644
index 00000000..d6bb17a8
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256Options.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.commons.compress.archivers.sevenz;
+
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Options for {@link SevenZMethod#AES256SHA256} encoder
+ *
+ * @since 1.23
+ * @see AES256SHA256Decoder
+ */
+class AES256Options {
+
+ private final byte[] salt;
+ private final byte[] iv;
+ private final int numCyclesPower;
+ private final Cipher cipher;
+
+ /**
+ * @param password password used for encryption
+ */
+ public AES256Options(char[] password) {
+ this(password, new byte[0], randomBytes(16), 19);
+ }
+
+ /**
+ * @param password password used for encryption
+ * @param salt for password hash salting (enforce password security)
+ * @param iv Initialization Vector (IV) used by cipher algorithm
+ * @param numCyclesPower another password security enforcer parameter that controls the cycles of password hashing. More the
+ * this number is high, more security you'll have but also high CPU usage
+ */
+ public AES256Options(char[] password, byte[] salt, byte[] iv, int numCyclesPower) {
+ this.salt = salt;
+ this.iv = iv;
+ this.numCyclesPower = numCyclesPower;
+
+ // NOTE: for security purposes, password is wrapped in a Cipher as soon as possible to not stay in memory
+ final byte[] aesKeyBytes = AES256SHA256Decoder.sha256Password(password, numCyclesPower, salt);
+ final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
+
+ try {
+ cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv));
+ } catch (final GeneralSecurityException generalSecurityException) {
+ throw new IllegalStateException(
+ "Encryption error (do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
+ generalSecurityException
+ );
+ }
+ }
+
+ byte[] getIv() {
+ return iv;
+ }
+
+ int getNumCyclesPower() {
+ return numCyclesPower;
+ }
+
+ byte[] getSalt() {
+ return salt;
+ }
+
+ Cipher getCipher() {
+ return cipher;
+ }
+
+ private static byte[] randomBytes(int size) {
+ byte[] bytes = new byte[size];
+ try {
+ SecureRandom.getInstanceStrong().nextBytes(bytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("No strong secure random available to generate strong AES key", e);
+ }
+ return bytes;
+ }
+}
diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256SHA256Decoder.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256SHA256Decoder.java
index 8fb5a778..19d43443 100644
--- a/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256SHA256Decoder.java
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256SHA256Decoder.java
@@ -17,14 +17,21 @@
*/
package org.apache.commons.compress.archivers.sevenz;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@@ -32,6 +39,10 @@ import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.compress.PasswordRequiredException;
class AES256SHA256Decoder extends AbstractCoder {
+
+ AES256SHA256Decoder() {
+ super(AES256Options.class);
+ }
@Override
InputStream decode(final String archiveName, final InputStream in, final long uncompressedLength,
@@ -73,26 +84,7 @@ class AES256SHA256Decoder extends AbstractCoder {
System.arraycopy(passwordBytes, 0, aesKeyBytes, saltSize,
Math.min(passwordBytes.length, aesKeyBytes.length - saltSize));
} else {
- final MessageDigest digest;
- try {
- digest = MessageDigest.getInstance("SHA-256");
- } catch (final NoSuchAlgorithmException noSuchAlgorithmException) {
- throw new IOException("SHA-256 is unsupported by your Java implementation",
- noSuchAlgorithmException);
- }
- final byte[] extra = new byte[8];
- for (long j = 0; j < (1L << numCyclesPower); j++) {
- digest.update(salt);
- digest.update(passwordBytes);
- digest.update(extra);
- for (int k = 0; k < extra.length; k++) {
- ++extra[k];
- if (extra[k] != 0) {
- break;
- }
- }
- }
- aesKeyBytes = digest.digest();
+ aesKeyBytes = sha256Password(passwordBytes, numCyclesPower, salt);
}
final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
@@ -103,8 +95,8 @@ class AES256SHA256Decoder extends AbstractCoder {
isInitialized = true;
return cipherInputStream;
} catch (final GeneralSecurityException generalSecurityException) {
- throw new IOException("Decryption error " +
- "(do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
+ throw new IllegalStateException(
+ "Decryption error (do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
generalSecurityException);
}
}
@@ -127,4 +119,133 @@ class AES256SHA256Decoder extends AbstractCoder {
}
};
}
+
+ @Override
+ OutputStream encode(OutputStream out, Object options) throws IOException {
+ final AES256Options opts = (AES256Options) options;
+
+ return new OutputStream() {
+ private final CipherOutputStream cipherOutputStream = new CipherOutputStream(out, opts.getCipher());
+
+ // Ensures that data are encrypt in respect of cipher block size and pad with '0' if smaller
+ // NOTE: As "AES/CBC/PKCS5Padding" is weak and should not be used, we use "AES/CBC/NoPadding" with this
+ // manual implementation for padding possible thanks to the size of the file stored separately
+ private final int cipherBlockSize = opts.getCipher().getBlockSize();
+ private final byte[] cipherBlockBuffer = new byte[cipherBlockSize];
+ private int count = 0;
+
+ @Override
+ public void write(int b) throws IOException {
+ cipherBlockBuffer[count++] = (byte) b;
+ if (count == cipherBlockSize) {
+ flushBuffer();
+ }
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ int gap = len + count > cipherBlockSize ? cipherBlockSize - count : len;
+ System.arraycopy(b, off, cipherBlockBuffer, count, gap);
+ count += gap;
+
+ if (count == cipherBlockSize) {
+ flushBuffer();
+
+ if (len - gap >= cipherBlockSize) {
+ // skip buffer to encrypt data chunks big enought to fit cipher block size
+ int multipleCipherBlockSizeLen = (len - gap) / cipherBlockSize * cipherBlockSize;
+ cipherOutputStream.write(b, off + gap, multipleCipherBlockSizeLen);
+ gap += multipleCipherBlockSizeLen;
+ }
+ System.arraycopy(b, off + gap, cipherBlockBuffer, 0, len - gap);
+ count = len - gap;
+ }
+ }
+
+ private void flushBuffer() throws IOException {
+ cipherOutputStream.write(cipherBlockBuffer);
+ count = 0;
+ Arrays.fill(cipherBlockBuffer, (byte) 0);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ cipherOutputStream.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (count > 0) {
+ cipherOutputStream.write(cipherBlockBuffer);
+ }
+ cipherOutputStream.close();
+ }
+ };
+ }
+
+ @Override
+ byte[] getOptionsAsProperties(Object options) throws IOException {
+ final AES256Options opts = (AES256Options) options;
+ final byte[] props = new byte[2 + opts.getSalt().length + opts.getIv().length];
+
+ // First byte : control (numCyclesPower + flags of salt or iv presence)
+ props[0] = (byte) (opts.getNumCyclesPower() | (opts.getSalt().length == 0 ? 0 : (1 << 7)) | (opts.getIv().length == 0 ? 0 : (1 << 6)));
+
+ if (opts.getSalt().length != 0 || opts.getIv().length != 0) {
+ // second byte : size of salt/iv data
+ props[1] = (byte) (((opts.getSalt().length == 0 ? 0 : opts.getSalt().length - 1) << 4) | (opts.getIv().length == 0 ? 0 : opts.getIv().length - 1));
+
+ // remain bytes : salt/iv data
+ System.arraycopy(opts.getSalt(), 0, props, 2, opts.getSalt().length);
+ System.arraycopy(opts.getIv(), 0, props, 2 + opts.getSalt().length, opts.getIv().length);
+ }
+
+ return props;
+ }
+
+ static byte[] sha256Password(final char[] password, final int numCyclesPower, final byte[] salt) {
+ return sha256Password(utf16Decode(password), numCyclesPower, salt);
+ }
+
+ static byte[] sha256Password(final byte[] password, final int numCyclesPower, final byte[] salt) {
+ final MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("SHA-256");
+ } catch (final NoSuchAlgorithmException noSuchAlgorithmException) {
+ throw new IllegalStateException("SHA-256 is unsupported by your Java implementation", noSuchAlgorithmException);
+ }
+ final byte[] extra = new byte[8];
+ for (long j = 0; j < (1L << numCyclesPower); j++) {
+ digest.update(salt);
+ digest.update(password);
+ digest.update(extra);
+ for (int k = 0; k < extra.length; k++) {
+ ++extra[k];
+ if (extra[k] != 0) {
+ break;
+ }
+ }
+ }
+ return digest.digest();
+ }
+
+ /**
+ * Convenience method that encodes Unicode characters into bytes in UTF-16 (ittle-endian byte order) charset
+ *
+ * @param chars characters to encode
+ * @return encoded characters
+ * @since 1.23
+ */
+ static byte[] utf16Decode(final char[] chars) {
+ if (chars == null) {
+ return null;
+ }
+ final ByteBuffer encoded = UTF_16LE.encode(CharBuffer.wrap(chars));
+ if (encoded.hasArray()) {
+ return encoded.array();
+ }
+ final byte[] e = new byte[encoded.remaining()];
+ encoded.get(e);
+ return e;
+ }
}
diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
index 002a5da0..a7d938d5 100644
--- a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java
@@ -30,7 +30,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
@@ -137,7 +136,7 @@ public class SevenZFile implements Closeable {
*/
public SevenZFile(final File fileName, final char[] password, final SevenZFileOptions options) throws IOException {
this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), // NOSONAR
- fileName.getAbsolutePath(), utf16Decode(password), true, options);
+ fileName.getAbsolutePath(), AES256SHA256Decoder.utf16Decode(password), true, options);
}
/**
@@ -256,7 +255,7 @@ public class SevenZFile implements Closeable {
*/
public SevenZFile(final SeekableByteChannel channel, final String fileName, final char[] password,
final SevenZFileOptions options) throws IOException {
- this(channel, fileName, utf16Decode(password), false, options);
+ this(channel, fileName, AES256SHA256Decoder.utf16Decode(password), false, options);
}
/**
@@ -2056,19 +2055,6 @@ public class SevenZFile implements Closeable {
return lastSegment + "~";
}
- private static byte[] utf16Decode(final char[] chars) {
- if (chars == null) {
- return null;
- }
- final ByteBuffer encoded = UTF_16LE.encode(CharBuffer.wrap(chars));
- if (encoded.hasArray()) {
- return encoded.array();
- }
- final byte[] e = new byte[encoded.remaining()];
- encoded.get(e);
- return e;
- }
-
private static int assertFitsIntoNonNegativeInt(final String what, final long value) throws IOException {
if (value > Integer.MAX_VALUE || value < 0) {
throw new IOException("Cannot handle " + what + " " + value);
diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java
index 238dfb5c..14080640 100644
--- a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java
@@ -47,8 +47,10 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import java.util.zip.CRC32;
-
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.utils.CountingOutputStream;
import org.apache.commons.compress.utils.TimeUtils;
@@ -70,6 +72,7 @@ public class SevenZOutputFile implements Closeable {
private Iterable<? extends SevenZMethodConfiguration> contentMethods =
Collections.singletonList(new SevenZMethodConfiguration(SevenZMethod.LZMA2));
private final Map<SevenZArchiveEntry, long[]> additionalSizes = new HashMap<>();
+ private AES256Options aes256Options;
/**
* Opens file to write a 7z archive to.
@@ -78,9 +81,25 @@ public class SevenZOutputFile implements Closeable {
* @throws IOException if opening the file fails
*/
public SevenZOutputFile(final File fileName) throws IOException {
- this(Files.newByteChannel(fileName.toPath(),
- EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE,
- StandardOpenOption.TRUNCATE_EXISTING)));
+ this(fileName, null);
+ }
+
+ /**
+ * Opens file to write a 7z archive to.
+ *
+ * @param fileName the file to write to
+ * @param password optional password if the archive has to be encrypted
+ * @throws IOException if opening the file fails
+ * @since 1.23
+ */
+ public SevenZOutputFile(final File fileName, char[] password) throws IOException {
+ this(
+ Files.newByteChannel(
+ fileName.toPath(),
+ EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)
+ ),
+ password
+ );
}
/**
@@ -95,8 +114,27 @@ public class SevenZOutputFile implements Closeable {
* @since 1.13
*/
public SevenZOutputFile(final SeekableByteChannel channel) throws IOException {
+ this(channel, null);
+ }
+
+ /**
+ * Prepares channel to write a 7z archive to.
+ *
+ * <p>{@link
+ * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
+ * allows you to write to an in-memory archive.</p>
+ *
+ * @param channel the channel to write to
+ * @param password optional password if the archive has to be encrypted
+ * @throws IOException if the channel cannot be positioned properly
+ * @since 1.23
+ */
+ public SevenZOutputFile(final SeekableByteChannel channel, char[] password) throws IOException {
this.channel = channel;
channel.position(SevenZFile.SIGNATURE_HEADER_SIZE);
+ if (password != null) {
+ this.aes256Options = new AES256Options(password);
+ }
}
/**
@@ -413,7 +451,19 @@ public class SevenZOutputFile implements Closeable {
private Iterable<? extends SevenZMethodConfiguration> getContentMethods(final SevenZArchiveEntry entry) {
final Iterable<? extends SevenZMethodConfiguration> ms = entry.getContentMethods();
- return ms == null ? contentMethods : ms;
+ Iterable<? extends SevenZMethodConfiguration> iter = ms == null ? contentMethods : ms;
+
+ if (aes256Options != null) {
+ // prepend encryption
+ iter =
+ Stream
+ .concat(
+ Stream.of(new SevenZMethodConfiguration(SevenZMethod.AES256SHA256, aes256Options)),
+ StreamSupport.stream(iter.spliterator(), false)
+ )
+ .collect(Collectors.toList());
+ }
+ return iter;
}
private void writeHeader(final DataOutput header) throws IOException {
@@ -532,15 +582,15 @@ public class SevenZOutputFile implements Closeable {
private void writeSubStreamsInfo(final DataOutput header) throws IOException {
header.write(NID.kSubStreamsInfo);
-//
-// header.write(NID.kCRC);
-// header.write(1);
-// for (final SevenZArchiveEntry entry : files) {
-// if (entry.getHasCrc()) {
-// header.writeInt(Integer.reverseBytes(entry.getCrc()));
-// }
-// }
-//
+ //
+ // header.write(NID.kCRC);
+ // header.write(1);
+ // for (final SevenZArchiveEntry entry : files) {
+ // if (entry.getHasCrc()) {
+ // header.writeInt(Integer.reverseBytes(entry.getCrc()));
+ // }
+ // }
+ //
header.write(NID.kEnd);
}
diff --git a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java
index 908951fd..53b16543 100644
--- a/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFileTest.java
@@ -20,8 +20,11 @@ package org.apache.commons.compress.archivers.sevenz;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.apache.commons.compress.utils.TimeUtils;
import org.junit.jupiter.api.Test;
@@ -41,6 +44,7 @@ import java.util.Date;
import java.util.Iterator;
import org.apache.commons.compress.AbstractTestCase;
+import org.apache.commons.compress.PasswordRequiredException;
import org.apache.commons.compress.utils.ByteUtils;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.tukaani.xz.LZMA2Options;
@@ -488,6 +492,42 @@ public class SevenZOutputFileTest extends AbstractTestCase {
}
}
+ /**
+ * Test password-based encryption
+ *
+ * <p>
+ * As AES/CBC Cipher requires a minimum of 16 bytes file data to be encrypted, some padding logic has been implemented.
+ * This test checks different file sizes (1, 16..) to ensure code coverage
+ * </p>
+ */
+ @Test
+ public void testEncrypt() throws Exception {
+ output = new File(dir, "encrypted.7z");
+ try (SevenZOutputFile outArchive = new SevenZOutputFile(output, "foo".toCharArray())) {
+ addFile(outArchive, 0, 1, null);
+ addFile(outArchive, 1, 16, null);
+ addFile(outArchive, 2, 32, null);
+ addFile(outArchive, 3, 33, null);
+ addFile(outArchive, 4, 10000, null);
+ }
+
+ // Is archive really password-based encrypted ?
+ try (SevenZFile archive = new SevenZFile(output)) {
+ assertThrows(
+ "A password should be needed",
+ PasswordRequiredException.class,
+ () -> verifyFile(archive, 0));
+ }
+
+ try (SevenZFile archive = new SevenZFile(output, "foo".toCharArray())) {
+ assertEquals(Boolean.TRUE, verifyFile(archive, 0, 1, null));
+ assertEquals(Boolean.TRUE, verifyFile(archive, 1, 16, null));
+ assertEquals(Boolean.TRUE, verifyFile(archive, 2, 32, null));
+ assertEquals(Boolean.TRUE, verifyFile(archive, 3, 33, null));
+ assertEquals(Boolean.TRUE, verifyFile(archive, 4, 10000, null));
+ }
+ }
+
private void testCompress252(final int numberOfFiles, final int numberOfNonEmptyFiles)
throws Exception {
final int nonEmptyModulus = numberOfNonEmptyFiles != 0
@@ -542,21 +582,39 @@ public class SevenZOutputFileTest extends AbstractTestCase {
}
private void addFile(final SevenZOutputFile archive, final int index, final boolean nonEmpty, final Iterable<SevenZMethodConfiguration> methods)
+ throws Exception {
+ addFile(archive, index, nonEmpty ? 1 : 0, methods);
+ }
+
+ private void addFile(final SevenZOutputFile archive, final int index, final int size, final Iterable<SevenZMethodConfiguration> methods)
throws Exception {
final SevenZArchiveEntry entry = new SevenZArchiveEntry();
entry.setName("foo/" + index + ".txt");
entry.setContentMethods(methods);
archive.putArchiveEntry(entry);
- archive.write(nonEmpty ? new byte[] { 'A' } : new byte[0]);
+ archive.write(generateFileData(size));
archive.closeArchiveEntry();
}
+ private byte[] generateFileData(int size) {
+ byte[] data = new byte[size];
+ for (int i = 0; i < size; i++) {
+ data[i] = (byte) ('A' + (i % 26));
+ }
+ return data;
+ }
+
private Boolean verifyFile(final SevenZFile archive, final int index) throws Exception {
return verifyFile(archive, index, null);
}
private Boolean verifyFile(final SevenZFile archive, final int index,
final Iterable<SevenZMethodConfiguration> methods) throws Exception {
+ return verifyFile(archive, index, 1, methods);
+ }
+
+ private Boolean verifyFile(final SevenZFile archive, final int index, final int size,
+ final Iterable<SevenZMethodConfiguration> methods) throws Exception {
final SevenZArchiveEntry entry = archive.getNextEntry();
if (entry == null) {
return null;
@@ -566,8 +624,16 @@ public class SevenZOutputFileTest extends AbstractTestCase {
if (entry.getSize() == 0) {
return Boolean.FALSE;
}
- assertEquals(1, entry.getSize());
- assertEquals('A', archive.read());
+ assertEquals(size, entry.getSize());
+
+ byte[] actual = new byte[size];
+ int count = 0;
+ while (count < size) {
+ int read = archive.read(actual, count, actual.length - count);
+ assertNotEquals(-1, read, "EOF reached before reading all expected data");
+ count += read;
+ }
+ assertArrayEquals(generateFileData(size), actual);
assertEquals(-1, archive.read());
if (methods != null) {
assertContentMethodsEquals(methods, entry.getContentMethods());