diff --git a/src/main/java/nc/opt/util/J7zip.java b/src/main/java/nc/opt/util/J7zip.java index c369041..bd11a69 100644 --- a/src/main/java/nc/opt/util/J7zip.java +++ b/src/main/java/nc/opt/util/J7zip.java @@ -54,22 +54,18 @@ public Integer call() throws Exception { J7zip.decompress(names[1], names[2], password, command == Command.x); } else if (command == Command.a) { - if (password != null) { - System.err.println("password protection is only applicable on decompression"); - return 1; - } if (names.length < 2) { System.err.println("Destination archive file name or source dir(s)/file(s) missing"); return 1; } - J7zip.compress(names[1], Stream.of(names).skip(2).map(File::new).toArray(File[]::new)); + J7zip.compress(names[1], password, Stream.of(names).skip(2).map(File::new).toArray(File[]::new)); } return 0; } - public static void compress(String name, File... files) throws IOException { - try (SevenZOutputFile out = new SevenZOutputFile(new File(name))) { + public static void compress(String name, String password, File... files) throws IOException { + try (SevenZOutputFile out = new SevenZOutputFile(new File(name), password != null ? password.toCharArray() : null)) { for (File file : files) { addToArchiveCompression(out, file, file.getParent()); } diff --git a/src/main/java/org/apache/commons/compress/MemoryLimitException.java b/src/main/java/org/apache/commons/compress/MemoryLimitException.java new file mode 100644 index 0000000..49e17f0 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/MemoryLimitException.java @@ -0,0 +1,64 @@ +/* + * 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; + +import java.io.IOException; + +/** + * If a stream checks for estimated memory allocation, and the estimate + * goes above the memory limit, this is thrown. This can also be thrown + * if a stream tries to allocate a byte array that is larger than + * the allowable limit. + * + * @since 1.14 + */ +public class MemoryLimitException extends IOException { + + private static final long serialVersionUID = 1L; + + //long instead of int to account for overflow for corrupt files + private final long memoryNeededInKb; + private final int memoryLimitInKb; + + public MemoryLimitException(final long memoryNeededInKb, final int memoryLimitInKb) { + super(buildMessage(memoryNeededInKb, memoryLimitInKb)); + this.memoryNeededInKb = memoryNeededInKb; + this.memoryLimitInKb = memoryLimitInKb; + } + + public MemoryLimitException(final long memoryNeededInKb, final int memoryLimitInKb, final Exception e) { + super(buildMessage(memoryNeededInKb, memoryLimitInKb), e); + this.memoryNeededInKb = memoryNeededInKb; + this.memoryLimitInKb = memoryLimitInKb; + } + + public long getMemoryNeededInKb() { + return memoryNeededInKb; + } + + public int getMemoryLimitInKb() { + return memoryLimitInKb; + } + + private static String buildMessage(final long memoryNeededInKb, final int memoryLimitInKb) { + return memoryNeededInKb + " kb of memory would be needed; limit was " + + memoryLimitInKb + " kb. " + + "If the file is not corrupt, consider increasing the memory limit."; + } +} diff --git a/src/main/java/org/apache/commons/compress/PasswordRequiredException.java b/src/main/java/org/apache/commons/compress/PasswordRequiredException.java new file mode 100644 index 0000000..d876b96 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/PasswordRequiredException.java @@ -0,0 +1,40 @@ +/* + * 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; + +import java.io.IOException; + +/** + * Exception thrown when trying to read an encrypted entry or file without + * configuring a password. + * @since 1.10 + */ +public class PasswordRequiredException extends IOException { + + private static final long serialVersionUID = 1391070005491684483L; + + /** + * Create a new exception. + * + * @param name name of the archive containing encrypted streams or + * the encrypted file. + */ + public PasswordRequiredException(final String name) { + super("Cannot read encrypted content from " + name + " without a password."); + } +} diff --git a/src/main/java/org/apache/commons/compress/archivers/ArchiveEntry.java b/src/main/java/org/apache/commons/compress/archivers/ArchiveEntry.java new file mode 100644 index 0000000..d5fa746 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/ArchiveEntry.java @@ -0,0 +1,61 @@ +/* + * 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; + +import java.util.Date; + +/** + * Represents an entry of an archive. + */ +public interface ArchiveEntry { + + /** + * Gets the name of the entry in this archive. May refer to a file or directory or other item. + * + *
This method returns the raw name as it is stored inside of the archive.
+ * + * @return The name of this entry in the archive. + */ + String getName(); + + /** + * Gets the uncompressed size of this entry. May be -1 (SIZE_UNKNOWN) if the size is unknown + * + * @return the uncompressed size of this entry. + */ + long getSize(); + + /** Special value indicating that the size is unknown */ + long SIZE_UNKNOWN = -1; + + /** + * Returns true if this entry refers to a directory. + * + * @return true if this entry refers to a directory. + */ + boolean isDirectory(); + + /** + * Gets the last modified date of this entry. + * + * @return the last modified date of this entry. + * @since 1.1 + */ + Date getLastModifiedDate(); +} 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 0000000..2aee902 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256Options.java @@ -0,0 +1,17 @@ +package org.apache.commons.compress.archivers.sevenz; + +import java.util.Arrays; +import java.util.Random; + +public class AES256Options { + byte[] password; + byte[] salt = new byte[0]; + byte[] iv = new byte[16]; + int numCyclesPower = 19; + + public AES256Options(byte[] password) { + this.password = password; + new Random(Arrays.hashCode(password)).nextBytes(salt); + new Random(Arrays.hashCode(password)).nextBytes(iv); + } +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256SHA256Coder.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256SHA256Coder.java new file mode 100644 index 0000000..02bfde6 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/AES256SHA256Coder.java @@ -0,0 +1,238 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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; +import org.apache.commons.compress.PasswordRequiredException; + +class AES256SHA256Coder extends CoderBase { + + AES256SHA256Coder() { + super(AES256Options.class); + } + + @Override + InputStream decode( + final String archiveName, + final InputStream in, + final long uncompressedLength, + final Coder coder, + final byte[] passwordBytes, + final int maxMemoryLimitInKb + ) { + return new InputStream() { + private boolean isInitialized; + private CipherInputStream cipherInputStream; + + private CipherInputStream init() throws IOException { + if (isInitialized) { + return cipherInputStream; + } + if (coder.properties == null) { + throw new IOException("Missing AES256 properties in " + archiveName); + } + if (coder.properties.length < 2) { + throw new IOException("AES256 properties too short in " + archiveName); + } + final int byte0 = 0xff & coder.properties[0]; + final int numCyclesPower = byte0 & 0x3f; + final int byte1 = 0xff & coder.properties[1]; + final int ivSize = ((byte0 >> 6) & 1) + (byte1 & 0x0f); + final int saltSize = ((byte0 >> 7) & 1) + (byte1 >> 4); + if (2 + saltSize + ivSize > coder.properties.length) { + throw new IOException("Salt size + IV size too long in " + archiveName); + } + final byte[] salt = new byte[saltSize]; + System.arraycopy(coder.properties, 2, salt, 0, saltSize); + final byte[] iv = new byte[16]; + System.arraycopy(coder.properties, 2 + saltSize, iv, 0, ivSize); + + if (passwordBytes == null) { + throw new PasswordRequiredException(archiveName); + } + final byte[] aesKeyBytes; + if (numCyclesPower == 0x3f) { + aesKeyBytes = new byte[32]; + System.arraycopy(salt, 0, aesKeyBytes, 0, saltSize); + 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(); + } + + System.out.println(aesKeyBytes); + + final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES"); + try { + final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(iv)); + cipherInputStream = new CipherInputStream(in, cipher); + isInitialized = true; + return cipherInputStream; + } catch (final GeneralSecurityException generalSecurityException) { + throw new IOException( + "Decryption error " + "(do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)", + generalSecurityException + ); + } + } + + @Override + public int read() throws IOException { + return init().read(); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + return init().read(b, off, len); + } + + @Override + public void close() throws IOException { + if (cipherInputStream != null) { + cipherInputStream.close(); + } + } + }; + } + + @Override + OutputStream encode(OutputStream out, Object options) throws IOException { + return new OutputStream() { + private boolean isInitialized; + private CipherOutputStream cipherOutputStream; + + private CipherOutputStream init() throws IOException { + if (isInitialized) { + return cipherOutputStream; + } + + AES256Options opts = (AES256Options) options; + + 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 << opts.numCyclesPower); j++) { + digest.update(opts.salt); + digest.update(opts.password); + digest.update(extra); + for (int k = 0; k < extra.length; k++) { + ++extra[k]; + if (extra[k] != 0) { + break; + } + } + } + final byte[] aesKeyBytes = digest.digest(); + System.out.println(aesKeyBytes); + + final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES"); + try { + final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(opts.iv)); + cipherOutputStream = new CipherOutputStream(out, cipher); + isInitialized = true; + return cipherOutputStream; + } catch (final GeneralSecurityException generalSecurityException) { + throw new IOException( + "Decryption error " + "(do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)", + generalSecurityException + ); + } + } + + @Override + public void write(int b) throws IOException { + init().write(b); + } + + @Override + public void write(byte[] b) throws IOException { + init().write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + init().write(b, off, len); + } + + @Override + public void close() throws IOException { + if (cipherOutputStream != null) { + cipherOutputStream.close(); + } + } + }; + } + + @Override + byte[] getOptionsAsProperties(Object options) throws IOException { + AES256Options opts = (AES256Options) options; + byte[] props = new byte[2 + opts.salt.length + opts.iv.length]; + + props[0] = (byte) (opts.numCyclesPower | (opts.salt.length == 0 ? 0 : (1 << 7)) | (opts.iv.length == 0 ? 0 : (1 << 6))); + + if (opts.salt.length != 0 || opts.iv.length != 0) { + props[1] = (byte) (((opts.salt.length == 0 ? 0 : opts.salt.length - 1) << 4) | (opts.iv.length == 0 ? 0 : opts.iv.length - 1)); + + System.arraycopy(opts.salt, 0, props, 2, opts.salt.length); + System.arraycopy(opts.iv, 0, props, 2 + opts.salt.length, opts.iv.length); + } + + return props; + } +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/Archive.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/Archive.java new file mode 100644 index 0000000..7d3d86a --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/Archive.java @@ -0,0 +1,55 @@ +/* + * 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.util.BitSet; + +class Archive { + /// Offset from beginning of file + SIGNATURE_HEADER_SIZE to packed streams. + long packPos; + /// Size of each packed stream. + long[] packSizes = new long[0]; + /// Whether each particular packed streams has a CRC. + BitSet packCrcsDefined; + /// CRCs for each packed stream, valid only if that packed stream has one. + long[] packCrcs; + /// Properties of solid compression blocks. + Folder[] folders = Folder.EMPTY_FOLDER_ARRAY; + /// Temporary properties for non-empty files (subsumed into the files array later). + SubStreamsInfo subStreamsInfo; + /// The files and directories in the archive. + SevenZArchiveEntry[] files = SevenZArchiveEntry.EMPTY_SEVEN_Z_ARCHIVE_ENTRY_ARRAY; + /// Mapping between folders, files and streams. + StreamMap streamMap; + + @Override + public String toString() { + return "Archive with packed streams starting at offset " + packPos + + ", " + lengthOf(packSizes) + " pack sizes, " + lengthOf(packCrcs) + + " CRCs, " + lengthOf(folders) + " folders, " + lengthOf(files) + + " files and " + streamMap; + } + + private static String lengthOf(final long[] a) { + return a == null ? "(null)" : String.valueOf(a.length); + } + + private static String lengthOf(final Object[] a) { + return a == null ? "(null)" : String.valueOf(a.length); + } +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/BindPair.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/BindPair.java new file mode 100644 index 0000000..2710b72 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/BindPair.java @@ -0,0 +1,28 @@ +/* + * 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; + +class BindPair { + long inIndex; + long outIndex; + + @Override + public String toString() { + return "BindPair binding input " + inIndex + " to output " + outIndex; + } +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/BoundedSeekableByteChannelInputStream.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/BoundedSeekableByteChannelInputStream.java new file mode 100644 index 0000000..ca8b754 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/BoundedSeekableByteChannelInputStream.java @@ -0,0 +1,106 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +class BoundedSeekableByteChannelInputStream extends InputStream { + private static final int MAX_BUF_LEN = 8192; + private final ByteBuffer buffer; + private final SeekableByteChannel channel; + private long bytesRemaining; + + public BoundedSeekableByteChannelInputStream(final SeekableByteChannel channel, + final long size) { + this.channel = channel; + this.bytesRemaining = size; + if (size < MAX_BUF_LEN && size > 0) { + buffer = ByteBuffer.allocate((int) size); + } else { + buffer = ByteBuffer.allocate(MAX_BUF_LEN); + } + } + + @Override + public int read() throws IOException { + if (bytesRemaining > 0) { + --bytesRemaining; + final int read = read(1); + if (read < 0) { + return read; + } + return buffer.get() & 0xff; + } + return -1; + } + + /** + * Reads up to len bytes of data from the input stream into an array of bytes. + * + *An attempt is made to read as many as len bytes, but a + * smaller number may be read. The number of bytes actually read + * is returned as an integer.
+ * + *This implementation may return 0 if the underlying {@link + * SeekableByteChannel} is non-blocking and currently hasn't got + * any bytes available.
+ */ + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (len == 0) { + return 0; + } + if (bytesRemaining <= 0) { + return -1; + } + int bytesToRead = len; + if (bytesToRead > bytesRemaining) { + bytesToRead = (int) bytesRemaining; + } + final int bytesRead; + final ByteBuffer buf; + if (bytesToRead <= buffer.capacity()) { + buf = buffer; + bytesRead = read(bytesToRead); + } else { + buf = ByteBuffer.allocate(bytesToRead); + bytesRead = channel.read(buf); + buf.flip(); + } + if (bytesRead >= 0) { + buf.get(b, off, bytesRead); + bytesRemaining -= bytesRead; + } + return bytesRead; + } + + private int read(final int len) throws IOException { + buffer.rewind().limit(len); + final int read = channel.read(buffer); + buffer.flip(); + return read; + } + + @Override + public void close() { + // the nested channel is controlled externally + } +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/CLI.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/CLI.java new file mode 100644 index 0000000..dfa1c58 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/CLI.java @@ -0,0 +1,107 @@ +/* + * 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.io.File; +import java.io.IOException; + +public class CLI { + + + private enum Mode { + LIST("Analysing") { + @Override + public void takeAction(final SevenZFile archive, final SevenZArchiveEntry entry) { + System.out.print(entry.getName()); + if (entry.isDirectory()) { + System.out.print(" dir"); + } else { + System.out.print(" " + entry.getCompressedSize() + + "/" + entry.getSize()); + } + if (entry.getHasLastModifiedDate()) { + System.out.print(" " + entry.getLastModifiedDate()); + } else { + System.out.print(" no last modified date"); + } + if (!entry.isDirectory()) { + System.out.println(" " + getContentMethods(entry)); + } else { + System.out.println(); + } + } + + private String getContentMethods(final SevenZArchiveEntry entry) { + final StringBuilder sb = new StringBuilder(); + boolean first = true; + for (final SevenZMethodConfiguration m : entry.getContentMethods()) { + if (!first) { + sb.append(", "); + } + first = false; + sb.append(m.getMethod()); + if (m.getOptions() != null) { + sb.append("(").append(m.getOptions()).append(")"); + } + } + return sb.toString(); + } + }; + + private final String message; + Mode(final String message) { + this.message = message; + } + public String getMessage() { + return message; + } + public abstract void takeAction(SevenZFile archive, SevenZArchiveEntry entry) + throws IOException; + } + + public static void main(final String[] args) throws Exception { + if (args.length == 0) { + usage(); + return; + } + final Mode mode = grabMode(args); + System.out.println(mode.getMessage() + " " + args[0]); + final File f = new File(args[0]); + if (!f.isFile()) { + System.err.println(f + " doesn't exist or is a directory"); + } + try (final SevenZFile archive = new SevenZFile(f)) { + SevenZArchiveEntry ae; + while((ae=archive.getNextEntry()) != null) { + mode.takeAction(archive, ae); + } + } + } + + private static void usage() { + System.out.println("Parameters: archive-name [list]"); + } + + private static Mode grabMode(final String[] args) { + if (args.length < 2) { + return Mode.LIST; + } + return Enum.valueOf(Mode.class, args[1].toUpperCase()); + } + +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/Coder.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/Coder.java new file mode 100644 index 0000000..e8cbe25 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/Coder.java @@ -0,0 +1,25 @@ +/* + * 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; + +class Coder { + byte[] decompressionMethodId; + long numInStreams; + long numOutStreams; + byte[] properties; +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/CoderBase.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/CoderBase.java new file mode 100644 index 0000000..fa3c5f4 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/CoderBase.java @@ -0,0 +1,85 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.compress.utils.ByteUtils; + +/** + * Base Codec class. + */ +abstract class CoderBase { + private final Class>[] acceptableOptions; + /** + * @param acceptableOptions types that can be used as options for this codec. + */ + protected CoderBase(final Class>... acceptableOptions) { + this.acceptableOptions = acceptableOptions; + } + + /** + * @return whether this method can extract options from the given object. + */ + boolean canAcceptOptions(final Object opts) { + for (final Class> c : acceptableOptions) { + if (c.isInstance(opts)) { + return true; + } + } + return false; + } + + /** + * @return property-bytes to write in a Folder block + */ + byte[] getOptionsAsProperties(final Object options) throws IOException { + return ByteUtils.EMPTY_BYTE_ARRAY; + } + + /** + * @return configuration options that have been used to create the given InputStream from the given Coder + */ + Object getOptionsFromCoder(final Coder coder, final InputStream in) throws IOException { + return null; + } + + /** + * @return a stream that reads from in using the configured coder and password. + */ + abstract InputStream decode(final String archiveName, + final InputStream in, long uncompressedLength, + final Coder coder, byte[] password, int maxMemoryLimitInKb) throws IOException; + + /** + * @return a stream that writes to out using the given configuration. + */ + OutputStream encode(final OutputStream out, final Object options) throws IOException { + throw new UnsupportedOperationException("Method doesn't support writing"); + } + + /** + * If the option represents a number, return its integer + * value, otherwise return the given default value. + */ + protected static int numberOptionOrDefault(final Object options, final int defaultValue) { + return options instanceof Number ? ((Number) options).intValue() : defaultValue; + } +} diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/Coders.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/Coders.java new file mode 100644 index 0000000..91910bf --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/Coders.java @@ -0,0 +1,230 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.SequenceInputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.apache.commons.compress.utils.FlushShieldFilterOutputStream; +import org.tukaani.xz.ARMOptions; +import org.tukaani.xz.ARMThumbOptions; +import org.tukaani.xz.FilterOptions; +import org.tukaani.xz.FinishableWrapperOutputStream; +import org.tukaani.xz.IA64Options; +import org.tukaani.xz.PowerPCOptions; +import org.tukaani.xz.SPARCOptions; +import org.tukaani.xz.X86Options; + +class Coders { + private static final MapThe first coder reads from the packed stream (we currently + * only support single input stream decoders), the second reads + * from the output of the first and so on.
+ */ + IterableThis method returns the raw name as it is stored inside of the archive.
+ * + * @return This entry's name. + */ + @Override + public String getName() { + return name; + } + + /** + * Set this entry's name. + * + * @param name This entry's new name. + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Whether there is any content associated with this entry. + * @return whether there is any content associated with this entry. + */ + public boolean hasStream() { + return hasStream; + } + + /** + * Sets whether there is any content associated with this entry. + * @param hasStream whether there is any content associated with this entry. + */ + public void setHasStream(final boolean hasStream) { + this.hasStream = hasStream; + } + + /** + * Return whether or not this entry represents a directory. + * + * @return True if this entry is a directory. + */ + @Override + public boolean isDirectory() { + return isDirectory; + } + + /** + * Sets whether or not this entry represents a directory. + * + * @param isDirectory True if this entry is a directory. + */ + public void setDirectory(final boolean isDirectory) { + this.isDirectory = isDirectory; + } + + /** + * Indicates whether this is an "anti-item" used in differential backups, + * meaning it should delete the same file from a previous backup. + * @return true if it is an anti-item, false otherwise + */ + public boolean isAntiItem() { + return isAntiItem; + } + + /** + * Sets whether this is an "anti-item" used in differential backups, + * meaning it should delete the same file from a previous backup. + * @param isAntiItem true if it is an anti-item, false otherwise + */ + public void setAntiItem(final boolean isAntiItem) { + this.isAntiItem = isAntiItem; + } + + /** + * Returns whether this entry has got a creation date at all. + * @return whether the entry has got a creation date + */ + public boolean getHasCreationDate() { + return hasCreationDate; + } + + /** + * Sets whether this entry has got a creation date at all. + * @param hasCreationDate whether the entry has got a creation date + */ + public void setHasCreationDate(final boolean hasCreationDate) { + this.hasCreationDate = hasCreationDate; + } + + /** + * Gets the creation date. + * @throws UnsupportedOperationException if the entry hasn't got a + * creation date. + * @return the creation date + */ + public Date getCreationDate() { + if (hasCreationDate) { + return ntfsTimeToJavaTime(creationDate); + } + throw new UnsupportedOperationException( + "The entry doesn't have this timestamp"); + } + + /** + * Sets the creation date using NTFS time (100 nanosecond units + * since 1 January 1601) + * @param ntfsCreationDate the creation date + */ + public void setCreationDate(final long ntfsCreationDate) { + this.creationDate = ntfsCreationDate; + } + + /** + * Sets the creation date, + * @param creationDate the creation date + */ + public void setCreationDate(final Date creationDate) { + hasCreationDate = creationDate != null; + if (hasCreationDate) { + this.creationDate = javaTimeToNtfsTime(creationDate); + } + } + + /** + * Returns whether this entry has got a last modified date at all. + * @return whether this entry has got a last modified date at all + */ + public boolean getHasLastModifiedDate() { + return hasLastModifiedDate; + } + + /** + * Sets whether this entry has got a last modified date at all. + * @param hasLastModifiedDate whether this entry has got a last + * modified date at all + */ + public void setHasLastModifiedDate(final boolean hasLastModifiedDate) { + this.hasLastModifiedDate = hasLastModifiedDate; + } + + /** + * Gets the last modified date. + * @throws UnsupportedOperationException if the entry hasn't got a + * last modified date. + * @return the last modified date + */ + @Override + public Date getLastModifiedDate() { + if (hasLastModifiedDate) { + return ntfsTimeToJavaTime(lastModifiedDate); + } + throw new UnsupportedOperationException( + "The entry doesn't have this timestamp"); + } + + /** + * Sets the last modified date using NTFS time (100 nanosecond + * units since 1 January 1601) + * @param ntfsLastModifiedDate the last modified date + */ + public void setLastModifiedDate(final long ntfsLastModifiedDate) { + this.lastModifiedDate = ntfsLastModifiedDate; + } + + /** + * Sets the last modified date, + * @param lastModifiedDate the last modified date + */ + public void setLastModifiedDate(final Date lastModifiedDate) { + hasLastModifiedDate = lastModifiedDate != null; + if (hasLastModifiedDate) { + this.lastModifiedDate = javaTimeToNtfsTime(lastModifiedDate); + } + } + + /** + * Returns whether this entry has got an access date at all. + * @return whether this entry has got an access date at all. + */ + public boolean getHasAccessDate() { + return hasAccessDate; + } + + /** + * Sets whether this entry has got an access date at all. + * @param hasAcessDate whether this entry has got an access date at all. + */ + public void setHasAccessDate(final boolean hasAcessDate) { + this.hasAccessDate = hasAcessDate; + } + + /** + * Gets the access date. + * @throws UnsupportedOperationException if the entry hasn't got a + * access date. + * @return the access date + */ + public Date getAccessDate() { + if (hasAccessDate) { + return ntfsTimeToJavaTime(accessDate); + } + throw new UnsupportedOperationException( + "The entry doesn't have this timestamp"); + } + + /** + * Sets the access date using NTFS time (100 nanosecond units + * since 1 January 1601) + * @param ntfsAccessDate the access date + */ + public void setAccessDate(final long ntfsAccessDate) { + this.accessDate = ntfsAccessDate; + } + + /** + * Sets the access date, + * @param accessDate the access date + */ + public void setAccessDate(final Date accessDate) { + hasAccessDate = accessDate != null; + if (hasAccessDate) { + this.accessDate = javaTimeToNtfsTime(accessDate); + } + } + + /** + * Returns whether this entry has windows attributes. + * @return whether this entry has windows attributes. + */ + public boolean getHasWindowsAttributes() { + return hasWindowsAttributes; + } + + /** + * Sets whether this entry has windows attributes. + * @param hasWindowsAttributes whether this entry has windows attributes. + */ + public void setHasWindowsAttributes(final boolean hasWindowsAttributes) { + this.hasWindowsAttributes = hasWindowsAttributes; + } + + /** + * Gets the windows attributes. + * @return the windows attributes + */ + public int getWindowsAttributes() { + return windowsAttributes; + } + + /** + * Sets the windows attributes. + * @param windowsAttributes the windows attributes + */ + public void setWindowsAttributes(final int windowsAttributes) { + this.windowsAttributes = windowsAttributes; + } + + /** + * Returns whether this entry has got a crc. + * + *In general entries without streams don't have a CRC either.
+ * @return whether this entry has got a crc. + */ + public boolean getHasCrc() { + return hasCrc; + } + + /** + * Sets whether this entry has got a crc. + * @param hasCrc whether this entry has got a crc. + */ + public void setHasCrc(final boolean hasCrc) { + this.hasCrc = hasCrc; + } + + /** + * Gets the CRC. + * @deprecated use getCrcValue instead. + * @return the CRC + */ + @Deprecated + public int getCrc() { + return (int) crc; + } + + /** + * Sets the CRC. + * @deprecated use setCrcValue instead. + * @param crc the CRC + */ + @Deprecated + public void setCrc(final int crc) { + this.crc = crc; + } + + /** + * Gets the CRC. + * @since 1.7 + * @return the CRC + */ + public long getCrcValue() { + return crc; + } + + /** + * Sets the CRC. + * @since 1.7 + * @param crc the CRC + */ + public void setCrcValue(final long crc) { + this.crc = crc; + } + + /** + * Gets the compressed CRC. + * @deprecated use getCompressedCrcValue instead. + * @return the compressed CRC + */ + @Deprecated + int getCompressedCrc() { + return (int) compressedCrc; + } + + /** + * Sets the compressed CRC. + * @deprecated use setCompressedCrcValue instead. + * @param crc the CRC + */ + @Deprecated + void setCompressedCrc(final int crc) { + this.compressedCrc = crc; + } + + /** + * Gets the compressed CRC. + * @since 1.7 + * @return the CRC + */ + long getCompressedCrcValue() { + return compressedCrc; + } + + /** + * Sets the compressed CRC. + * @since 1.7 + * @param crc the CRC + */ + void setCompressedCrcValue(final long crc) { + this.compressedCrc = crc; + } + + /** + * Get this entry's file size. + * + * @return This entry's file size. + */ + @Override + public long getSize() { + return size; + } + + /** + * Set this entry's file size. + * + * @param size This entry's new file size. + */ + public void setSize(final long size) { + this.size = size; + } + + /** + * Get this entry's compressed file size. + * + * @return This entry's compressed file size. + */ + long getCompressedSize() { + return compressedSize; + } + + /** + * Set this entry's compressed file size. + * + * @param size This entry's new compressed file size. + */ + void setCompressedSize(final long size) { + this.compressedSize = size; + } + + /** + * Sets the (compression) methods to use for entry's content - the + * default is LZMA2. + * + *Currently only {@link SevenZMethod#COPY}, {@link + * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link + * SevenZMethod#DEFLATE} are supported when writing archives.
+ * + *The methods will be consulted in iteration order to create + * the final output.
+ * + * @param methods the methods to use for the content + * @since 1.8 + */ + public void setContentMethods(final Iterable extends SevenZMethodConfiguration> methods) { + if (methods != null) { + final LinkedListCurrently only {@link SevenZMethod#COPY}, {@link + * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link + * SevenZMethod#DEFLATE} are supported when writing archives.
+ * + *The methods will be consulted in iteration order to create + * the final output.
+ * + * @param methods the methods to use for the content + * @since 1.22 + */ + public void setContentMethods(SevenZMethodConfiguration... methods) { + setContentMethods(Arrays.asList(methods)); + } + + /** + * Gets the (compression) methods to use for entry's content - the + * default is LZMA2. + * + *Currently only {@link SevenZMethod#COPY}, {@link + * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link + * SevenZMethod#DEFLATE} are supported when writing archives.
+ * + *The methods will be consulted in iteration order to create + * the final output.
+ * + * @since 1.8 + * @return the methods to use for the content + */ + public Iterable extends SevenZMethodConfiguration> getContentMethods() { + return contentMethods; + } + + @Override + public int hashCode() { + final String n = getName(); + return n == null ? 0 : n.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final SevenZArchiveEntry other = (SevenZArchiveEntry) obj; + return + Objects.equals(name, other.name) && + hasStream == other.hasStream && + isDirectory == other.isDirectory && + isAntiItem == other.isAntiItem && + hasCreationDate == other.hasCreationDate && + hasLastModifiedDate == other.hasLastModifiedDate && + hasAccessDate == other.hasAccessDate && + creationDate == other.creationDate && + lastModifiedDate == other.lastModifiedDate && + accessDate == other.accessDate && + hasWindowsAttributes == other.hasWindowsAttributes && + windowsAttributes == other.windowsAttributes && + hasCrc == other.hasCrc && + crc == other.crc && + compressedCrc == other.compressedCrc && + size == other.size && + compressedSize == other.compressedSize && + equalSevenZMethods(contentMethods, other.contentMethods); + } + + /** + * Converts NTFS time (100 nanosecond units since 1 January 1601) + * to Java time. + * @param ntfsTime the NTFS time in 100 nanosecond units + * @return the Java time + */ + public static Date ntfsTimeToJavaTime(final long ntfsTime) { + final Calendar ntfsEpoch = Calendar.getInstance(); + ntfsEpoch.setTimeZone(TimeZone.getTimeZone("GMT+0")); + ntfsEpoch.set(1601, 0, 1, 0, 0, 0); + ntfsEpoch.set(Calendar.MILLISECOND, 0); + final long realTime = ntfsEpoch.getTimeInMillis() + (ntfsTime / (10*1000)); + return new Date(realTime); + } + + /** + * Converts Java time to NTFS time. + * @param date the Java time + * @return the NTFS time + */ + public static long javaTimeToNtfsTime(final Date date) { + final Calendar ntfsEpoch = Calendar.getInstance(); + ntfsEpoch.setTimeZone(TimeZone.getTimeZone("GMT+0")); + ntfsEpoch.set(1601, 0, 1, 0, 0, 0); + ntfsEpoch.set(Calendar.MILLISECOND, 0); + return ((date.getTime() - ntfsEpoch.getTimeInMillis())* 1000 * 10); + } + + private boolean equalSevenZMethods(final Iterable extends SevenZMethodConfiguration> c1, + final Iterable extends SevenZMethodConfiguration> c2) { + if (c1 == null) { + return c2 == null; + } + if (c2 == null) { + return false; + } + final Iterator extends SevenZMethodConfiguration> i2 = c2.iterator(); + for (SevenZMethodConfiguration element : c1) { + if (!i2.hasNext()) { + return false; + } + if (!element.equals(i2.next())) { + return false; + } + } + return !i2.hasNext(); + } +} 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 new file mode 100644 index 0000000..8ec1de0 --- /dev/null +++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZFile.java @@ -0,0 +1,2136 @@ +/* + * 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 static java.nio.charset.StandardCharsets.UTF_16LE; +import static org.apache.commons.compress.utils.ByteUtils.utf16Decode; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FilterInputStream; +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; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; + +import org.apache.commons.compress.MemoryLimitException; +import org.apache.commons.compress.utils.BoundedInputStream; +import org.apache.commons.compress.utils.ByteUtils; +import org.apache.commons.compress.utils.CRC32VerifyingInputStream; +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.compress.utils.InputStreamStatistics; + +/** + * Reads a 7z file, using SeekableByteChannel under + * the covers. + *+ * The 7z file format is a flexible container + * that can contain many compression and + * encryption types, but at the moment only + * only Copy, LZMA, LZMA2, BZIP2, Deflate and AES-256 + SHA-256 + * are supported. + *
+ * The format is very Windows/Intel specific, + * so it uses little-endian byte order, + * doesn't store user/group or permission bits, + * and represents times using NTFS timestamps + * (100 nanosecond units since 1 January 1601). + * Hence the official tools recommend against + * using it for backup purposes on *nix, and + * recommend .tar.7z or .tar.lzma or .tar.xz + * instead. + *
+ * Both the header and file contents may be + * compressed and/or encrypted. With both + * encrypted, neither file names nor file + * contents can be read, but the use of + * encryption isn't plausibly deniable. + * + *
Multi volume archives can be read by concatenating the parts in + * correct order - either manually or by using {link + * org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel} + * for example.
+ * + * @NotThreadSafe + * @since 1.6 + */ +public class SevenZFile implements Closeable { + static final int SIGNATURE_HEADER_SIZE = 32; + + private static final String DEFAULT_FILE_NAME = "unknown archive"; + + private final String fileName; + private SeekableByteChannel channel; + private final Archive archive; + private int currentEntryIndex = -1; + private int currentFolderIndex = -1; + private InputStream currentFolderInputStream; + private byte[] password; + private final SevenZFileOptions options; + + private long compressedBytesReadFromCurrentEntry; + private long uncompressedBytesReadFromCurrentEntry; + + private final ArrayList{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @throws IOException if reading the archive fails + * @since 1.13 + */ + public SevenZFile(final SeekableByteChannel channel) throws IOException { + this(channel, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a SeekableByteChannel as 7z archive with addtional options. + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final SeekableByteChannel channel, final SevenZFileOptions options) throws IOException { + this(channel, DEFAULT_FILE_NAME, null, options); + } + + /** + * Reads a SeekableByteChannel as 7z archive + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param password optional password if the archive is encrypted + * @throws IOException if reading the archive fails + * @since 1.17 + */ + public SevenZFile(final SeekableByteChannel channel, + final char[] password) throws IOException { + this(channel, password, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a SeekableByteChannel as 7z archive with additional options. + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param password optional password if the archive is encrypted + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final SeekableByteChannel channel, final char[] password, final SevenZFileOptions options) + throws IOException { + this(channel, DEFAULT_FILE_NAME, password, options); + } + + /** + * Reads a SeekableByteChannel as 7z archive + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param fileName name of the archive - only used for error reporting + * @param password optional password if the archive is encrypted + * @throws IOException if reading the archive fails + * @since 1.17 + */ + public SevenZFile(final SeekableByteChannel channel, final String fileName, + final char[] password) throws IOException { + this(channel, fileName, password, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a SeekableByteChannel as 7z archive with addtional options. + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param fileName name of the archive - only used for error reporting + * @param password optional password if the archive is encrypted + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final SeekableByteChannel channel, final String fileName, final char[] password, + final SevenZFileOptions options) throws IOException { + this(channel, fileName, utf16Decode(password), false, options); + } + + /** + * Reads a SeekableByteChannel as 7z archive + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param fileName name of the archive - only used for error reporting + * @throws IOException if reading the archive fails + * @since 1.17 + */ + public SevenZFile(final SeekableByteChannel channel, final String fileName) + throws IOException { + this(channel, fileName, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a SeekableByteChannel as 7z archive with additional options. + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param fileName name of the archive - only used for error reporting + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final SeekableByteChannel channel, final String fileName, final SevenZFileOptions options) + throws IOException { + this(channel, fileName, null, false, options); + } + + /** + * Reads a SeekableByteChannel as 7z archive + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param password optional password if the archive is encrypted - + * the byte array is supposed to be the UTF16-LE encoded + * representation of the password. + * @throws IOException if reading the archive fails + * @since 1.13 + * @deprecated use the char[]-arg version for the password instead + */ + @Deprecated + public SevenZFile(final SeekableByteChannel channel, + final byte[] password) throws IOException { + this(channel, DEFAULT_FILE_NAME, password); + } + + /** + * Reads a SeekableByteChannel as 7z archive + * + *{@link + * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} + * allows you to read from an in-memory archive.
+ * + * @param channel the channel to read + * @param fileName name of the archive - only used for error reporting + * @param password optional password if the archive is encrypted - + * the byte array is supposed to be the UTF16-LE encoded + * representation of the password. + * @throws IOException if reading the archive fails + * @since 1.13 + * @deprecated use the char[]-arg version for the password instead + */ + @Deprecated + public SevenZFile(final SeekableByteChannel channel, final String fileName, + final byte[] password) throws IOException { + this(channel, fileName, password, false, SevenZFileOptions.DEFAULT); + } + + private SevenZFile(final SeekableByteChannel channel, final String filename, + final byte[] password, final boolean closeOnError, final SevenZFileOptions options) throws IOException { + boolean succeeded = false; + this.channel = channel; + this.fileName = filename; + this.options = options; + try { + archive = readHeaders(password); + if (password != null) { + this.password = Arrays.copyOf(password, password.length); + } else { + this.password = null; + } + succeeded = true; + } finally { + if (!succeeded && closeOnError) { + this.channel.close(); + } + } + } + + /** + * Reads a file as unencrypted 7z archive + * + * @param fileName the file to read + * @throws IOException if reading the archive fails + */ + public SevenZFile(final File fileName) throws IOException { + this(fileName, SevenZFileOptions.DEFAULT); + } + + /** + * Reads a file as unencrypted 7z archive + * + * @param fileName the file to read + * @param options the options to apply + * @throws IOException if reading the archive fails or the memory limit (if set) is too small + * @since 1.19 + */ + public SevenZFile(final File fileName, final SevenZFileOptions options) throws IOException { + this(fileName, null, options); + } + + /** + * Closes the archive. + * @throws IOException if closing the file fails + */ + @Override + public void close() throws IOException { + if (channel != null) { + try { + channel.close(); + } finally { + channel = null; + if (password != null) { + Arrays.fill(password, (byte) 0); + } + password = null; + } + } + } + + /** + * Returns the next Archive Entry in this archive. + * + * @return the next entry, + * or {@code null} if there are no more entries + * @throws IOException if the next entry could not be read + */ + public SevenZArchiveEntry getNextEntry() throws IOException { + if (currentEntryIndex >= archive.files.length - 1) { + return null; + } + ++currentEntryIndex; + final SevenZArchiveEntry entry = archive.files[currentEntryIndex]; + if (entry.getName() == null && options.getUseDefaultNameForUnnamedEntries()) { + entry.setName(getDefaultName()); + } + buildDecodingStream(currentEntryIndex, false); + uncompressedBytesReadFromCurrentEntry = compressedBytesReadFromCurrentEntry = 0; + return entry; + } + + /** + * Returns a copy of meta-data of all archive entries. + * + *This method only provides meta-data, the entries can not be + * used to read the contents, you still need to process all + * entries in order using {@link #getNextEntry} for that.
+ * + *The content methods are only available for entries that have + * already been reached via {@link #getNextEntry}.
+ * + * @return a copy of meta-data of all archive entries. + * @since 1.11 + */ + public Iterable+ * 1. it's a random access + * 2. one of these 2 condition is meet : + *
+ * 2.1 currentEntryIndex != entryIndex : this means there are some entries + * to be skipped(currentEntryIndex < entryIndex) or the entry has already + * been read(currentEntryIndex > entryIndex) + *
+ * 2.2 currentEntryIndex == entryIndex && !hasCurrentEntryBeenRead:
+ * if the entry to be read is the current entry, but some data of it has
+ * been read before, then we need to reopen the stream of the folder and
+ * skip all the entries before the current entries
+ *
+ * @param entryIndex the entry to be read
+ * @param isInSameFolder are the entry to be read and the current entry in the same folder
+ * @param folderIndex the index of the folder which contains the entry
+ * @return true if there are entries actually skipped
+ * @throws IOException there are exceptions when skipping entries
+ * @since 1.21
+ */
+ private boolean skipEntriesWhenNeeded(final int entryIndex, final boolean isInSameFolder, final int folderIndex) throws IOException {
+ final SevenZArchiveEntry file = archive.files[entryIndex];
+ // if the entry to be read is the current entry, and the entry has not
+ // been read yet, then there's nothing we need to do
+ if (currentEntryIndex == entryIndex && !hasCurrentEntryBeenRead()) {
+ return false;
+ }
+
+ // 1. if currentEntryIndex < entryIndex :
+ // this means there are some entries to be skipped(currentEntryIndex < entryIndex)
+ // 2. if currentEntryIndex > entryIndex || (currentEntryIndex == entryIndex && hasCurrentEntryBeenRead) :
+ // this means the entry has already been read before, and we need to reopen the
+ // stream of the folder and skip all the entries before the current entries
+ int filesToSkipStartIndex = archive.streamMap.folderFirstFileIndex[currentFolderIndex];
+ if (isInSameFolder) {
+ if (currentEntryIndex < entryIndex) {
+ // the entries between filesToSkipStartIndex and currentEntryIndex had already been skipped
+ filesToSkipStartIndex = currentEntryIndex + 1;
+ } else {
+ // the entry is in the same folder of current entry, but it has already been read before, we need to reset
+ // the position of the currentFolderInputStream to the beginning of folder, and then skip the files
+ // from the start entry of the folder again
+ reopenFolderInputStream(folderIndex, file);
+ }
+ }
+
+ for (int i = filesToSkipStartIndex; i < entryIndex; i++) {
+ final SevenZArchiveEntry fileToSkip = archive.files[i];
+ InputStream fileStreamToSkip = new BoundedInputStream(currentFolderInputStream, fileToSkip.getSize());
+ if (fileToSkip.getHasCrc()) {
+ fileStreamToSkip = new CRC32VerifyingInputStream(fileStreamToSkip, fileToSkip.getSize(), fileToSkip.getCrcValue());
+ }
+ deferredBlockStreams.add(fileStreamToSkip);
+
+ // set the content methods as well, it equals to file.getContentMethods() because they are in same folder
+ fileToSkip.setContentMethods(file.getContentMethods());
+ }
+ return true;
+ }
+
+ /**
+ * Find out if any data of current entry has been read or not.
+ * This is achieved by comparing the bytes remaining to read
+ * and the size of the file.
+ *
+ * @return true if any data of current entry has been read
+ * @since 1.21
+ */
+ private boolean hasCurrentEntryBeenRead() {
+ boolean hasCurrentEntryBeenRead = false;
+ if (!deferredBlockStreams.isEmpty()) {
+ final InputStream currentEntryInputStream = deferredBlockStreams.get(deferredBlockStreams.size() - 1);
+ // get the bytes remaining to read, and compare it with the size of
+ // the file to figure out if the file has been read
+ if (currentEntryInputStream instanceof CRC32VerifyingInputStream) {
+ hasCurrentEntryBeenRead = ((CRC32VerifyingInputStream) currentEntryInputStream).getBytesRemaining() != archive.files[currentEntryIndex].getSize();
+ }
+
+ if (currentEntryInputStream instanceof BoundedInputStream) {
+ hasCurrentEntryBeenRead = ((BoundedInputStream) currentEntryInputStream).getBytesRemaining() != archive.files[currentEntryIndex].getSize();
+ }
+ }
+ return hasCurrentEntryBeenRead;
+ }
+
+ private InputStream buildDecoderStack(final Folder folder, final long folderOffset,
+ final int firstPackStreamIndex, final SevenZArchiveEntry entry) throws IOException {
+ channel.position(folderOffset);
+ InputStream inputStreamStack = new FilterInputStream(new BufferedInputStream(
+ new BoundedSeekableByteChannelInputStream(channel,
+ archive.packSizes[firstPackStreamIndex]))) {
+ @Override
+ public int read() throws IOException {
+ final int r = in.read();
+ if (r >= 0) {
+ count(1);
+ }
+ return r;
+ }
+ @Override
+ public int read(final byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+ @Override
+ public int read(final byte[] b, final int off, final int len) throws IOException {
+ if (len == 0) {
+ return 0;
+ }
+ final int r = in.read(b, off, len);
+ if (r >= 0) {
+ count(r);
+ }
+ return r;
+ }
+ private void count(final int c) {
+ compressedBytesReadFromCurrentEntry += c;
+ }
+ };
+ final LinkedList For archives using solid compression randomly accessing
+ * entries will be significantly slower than reading the archive
+ * sequentially. This implements the same heuristics the 7z tools use. In
+ * 7z's case if an archive contains entries without a name -
+ * i.e. {@link SevenZArchiveEntry#getName} returns {@code null} -
+ * then its command line and GUI tools will use this default name
+ * when extracting the entries. Not all codecs will honor this setting. Currently only lzma
+ * and lzma2 are supported. Not all codecs will honor this setting. Currently only lzma
+ * and lzma2 are supported. This special kind of broken archive is encountered when mutli volume archives are closed prematurely. If
+ * you enable this option SevenZFile will trust data that looks as if it could contain metadata of an archive
+ * and allocate big amounts of memory. It is strongly recommended to not enable this option without setting
+ * {@link #withMaxMemoryLimitInKb} at the same time.
+ *
+ * @param tryToRecoverBrokenArchives if true SevenZFile will try to recover archives that are broken in the
+ * specific way
+ * @return the reconfigured builder
+ * @since 1.21
+ */
+ public Builder withTryToRecoverBrokenArchives(final boolean tryToRecoverBrokenArchives) {
+ this.tryToRecoverBrokenArchives = tryToRecoverBrokenArchives;
+ return this;
+ }
+
+ /**
+ * Create the {@link SevenZFileOptions}.
+ *
+ * @return configured {@link SevenZFileOptions}.
+ */
+ public SevenZFileOptions build() {
+ return new SevenZFileOptions(maxMemoryLimitInKb, useDefaultNameForUnnamedEntries,
+ tryToRecoverBrokenArchives);
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZMethod.java b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZMethod.java
new file mode 100644
index 0000000..03d1166
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZMethod.java
@@ -0,0 +1,114 @@
+/*
+ * 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.util.Arrays;
+
+/**
+ * The (partially) supported compression/encryption methods used in 7z archives.
+ *
+ * All methods with a _FILTER suffix are used as preprocessors with
+ * the goal of creating a better compression ratio with the compressor
+ * that comes next in the chain of methods. 7z will in general only
+ * allow them to be used together with a "real" compression method but
+ * Commons Compress doesn't enforce this. The BCJ_ filters work on executable files for the given platform
+ * and convert relative addresses to absolute addresses in CALL
+ * instructions. This means they are only useful when applied to
+ * executables of the chosen platform. The exact type and interpretation of options depends on the
+ * method being configured. Currently supported are: {@link
+ * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
+ * allows you to write to an in-memory archive. {@link
+ * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
+ * allows you to write to an in-memory archive. Currently only {@link SevenZMethod#COPY}, {@link
+ * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link
+ * SevenZMethod#DEFLATE} are supported. This is a short form for passing a single-element iterable
+ * to {@link #setContentMethods}. Currently only {@link SevenZMethod#COPY}, {@link
+ * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link
+ * SevenZMethod#DEFLATE} are supported. The methods will be consulted in iteration order to create
+ * the final output. Provides classes for reading and writing archives using
+ the 7z format.
+ * For example:
+ * This method is used to clean up file names when they are
+ * used in exception messages as they may end up in log files or
+ * as console output and may have been read from a corrupted
+ * input. This includes the bytes read to fill the current cache and
+ * not read as bits so far. Typically used by our InputStreams that need to count the
+ * bytes read as well.
+ * Every implementation of the Java platform is required to support the following character encodings. Consult the
+ * release documentation for your implementation to see if any other encodings are supported. Consult the release
+ * documentation for your implementation to see if any other encodings are supported.
+ * This perhaps would best belong in the [lang] project. Even if a similar interface is defined in [lang], it is not
+ * foreseen that [compress] would be made to depend on [lang].
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark
+ * (either order accepted on input, big-endian used on output)
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Sixteen-bit Unicode Transformation Format, big-endian byte order.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Sixteen-bit Unicode Transformation Format, little-endian byte order.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Eight-bit Unicode Transformation Format.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Every implementation of the Java platform is required to support the following character encodings. Consult the
+ * release documentation for your implementation to see if any other encodings are supported. Consult the release
+ * documentation for your implementation to see if any other encodings are supported.
+ * This class best belongs in the Commons Lang or IO project. Even if a similar class is defined in another Commons
+ * component, it is not foreseen that Commons Compress would be made to depend on another Commons component.
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark
+ * (either order accepted on input, big-endian used on output)
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Sixteen-bit Unicode Transformation Format, big-endian byte order.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Sixteen-bit Unicode Transformation Format, little-endian byte order.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ *
+ * Eight-bit Unicode Transformation Format.
+ *
+ * Every implementation of the Java platform is required to support this character encoding.
+ * Will return the file name itself if it doesn't contain any
+ * dots. All leading directories of the {@code filename} parameter
+ * are skipped. Will return the file name itself if it doesn't contain any
+ * dots. All leading directories of the {@code filename} parameter
+ * are skipped. Will return an empty string if the file name doesn't contain
+ * any dots. Only the last segment of a the file name is consulted
+ * - i.e. all leading directories of the {@code filename}
+ * parameter are skipped. Will return an empty string if the file name doesn't contain
+ * any dots. Only the last segment of a the file name is consulted
+ * - i.e. all leading directories of the {@code filename}
+ * parameter are skipped. It can be be used to support output to devices such as tape drives that require output in this
+ * format. If the final block does not have enough content to fill an entire block, the output will
+ * be padded to a full block size. This class can be used to support TAR,PAX, and CPIO blocked output to character special devices.
+ * It is not recommended that this class be used unless writing to such devices, as the padding
+ * serves no useful purpose in such cases. This class should normally wrap a FileOutputStream or associated WritableByteChannel directly.
+ * If there is an intervening filter that modified the output, such as a CompressorOutputStream, or
+ * performs its own buffering, such as BufferedOutputStream, output to the device may
+ * no longer be of the specified size. Any content written to this stream should be self-delimiting and should tolerate any padding
+ * added to fill the last block. In a case where the stream's skip() method returns 0 before
+ * the requested number of bytes has been skip this implementation
+ * will fall back to using the read() method. This method will only skip less than the requested number of
+ * bytes if the end of the input stream has been reached. This method may invoke read repeatedly to fill the array and
+ * only read less bytes than the length of the array if the end of
+ * the stream has been reached. This method may invoke read repeatedly to fill the array and
+ * only read less bytes than the length of the array if the end of
+ * the stream has been reached. This method may invoke read repeatedly to read the bytes and
+ * only read less bytes than the requested length if the end of
+ * the stream has been reached. This method reads repeatedly from the channel until the
+ * requested number of bytes are read. This method blocks until
+ * the requested number of bytes are read, the end of the channel
+ * is detected, or an exception is thrown.
+ * This method buffers the input internally, so there is no need to use a
+ * {@code BufferedInputStream}.
+ *
+ * @param input the {@code InputStream} to read from
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since 1.5
+ */
+ public static byte[] toByteArray(final InputStream input) throws IOException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output);
+ return output.toByteArray();
+ }
+
+ /**
+ * Closes the given Closeable and swallows any IOException that may occur.
+ * @param c Closeable to close, can be null
+ * @since 1.7
+ */
+ public static void closeQuietly(final Closeable c) {
+ if (c != null) {
+ try {
+ c.close();
+ } catch (final IOException ignored) { // NOPMD NOSONAR
+ }
+ }
+ }
+
+ /**
+ * Copies the source file to the given output stream.
+ * @param sourceFile The file to read.
+ * @param outputStream The output stream to write.
+ * @throws IOException if an I/O error occurs when reading or writing.
+ * @since 1.21
+ */
+ public static void copy(final File sourceFile, final OutputStream outputStream) throws IOException {
+ Files.copy(sourceFile.toPath(), outputStream);
+ }
+
+ /**
+ * Copies part of the content of a InputStream into an OutputStream.
+ * Uses a default buffer size of 8024 bytes.
+ *
+ * @param input
+ * the InputStream to copy
+ * @param output
+ * the target Stream
+ * @param len
+ * maximum amount of bytes to copy
+ * @return the number of bytes copied
+ * @throws IOException
+ * if an error occurs
+ * @since 1.21
+ */
+ public static long copyRange(final InputStream input, final long len, final OutputStream output)
+ throws IOException {
+ return copyRange(input, len, output, COPY_BUF_SIZE);
+ }
+
+ /**
+ * Copies part of the content of a InputStream into an OutputStream
+ *
+ * @param input
+ * the InputStream to copy
+ * @param len
+ * maximum amount of bytes to copy
+ * @param output
+ * the target, may be null to simulate output to dev/null on Linux and NUL on Windows
+ * @param buffersize
+ * the buffer size to use, must be bigger than 0
+ * @return the number of bytes copied
+ * @throws IOException
+ * if an error occurs
+ * @throws IllegalArgumentException
+ * if buffersize is smaller than or equal to 0
+ * @since 1.21
+ */
+ public static long copyRange(final InputStream input, final long len, final OutputStream output,
+ final int buffersize) throws IOException {
+ if (buffersize < 1) {
+ throw new IllegalArgumentException("buffersize must be bigger than 0");
+ }
+ final byte[] buffer = new byte[(int) Math.min(buffersize, len)];
+ int n = 0;
+ long count = 0;
+ while (count < len && -1 != (n = input.read(buffer, 0, (int) Math.min(len - count, buffer.length)))) {
+ if (output != null) {
+ output.write(buffer, 0, n);
+ }
+ count += n;
+ }
+ return count;
+ }
+
+ /**
+ * Gets part of the contents of an {@code InputStream} as a {@code byte[]}.
+ *
+ * @param input the {@code InputStream} to read from
+ * @param len
+ * maximum amount of bytes to copy
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since 1.21
+ */
+ public static byte[] readRange(final InputStream input, final int len) throws IOException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copyRange(input, len, output);
+ return output.toByteArray();
+ }
+
+ /**
+ * Gets part of the contents of an {@code ReadableByteChannel} as a {@code byte[]}.
+ *
+ * @param input the {@code ReadableByteChannel} to read from
+ * @param len
+ * maximum amount of bytes to copy
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since 1.21
+ */
+ public static byte[] readRange(final ReadableByteChannel input, final int len) throws IOException {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final ByteBuffer b = ByteBuffer.allocate(Math.min(len, COPY_BUF_SIZE));
+ int read = 0;
+ while (read < len) {
+ // Make sure we never read more than len bytes
+ b.limit(Math.min(len - read, b.capacity()));
+ final int readNow = input.read(b);
+ if (readNow <= 0) {
+ break;
+ }
+ output.write(b.array(), 0, readNow);
+ b.rewind();
+ read += readNow;
+ }
+ return output.toByteArray();
+ }
+
+}
diff --git a/src/main/java/org/apache/commons/compress/utils/InputStreamStatistics.java b/src/main/java/org/apache/commons/compress/utils/InputStreamStatistics.java
new file mode 100644
index 0000000..569ab36
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/utils/InputStreamStatistics.java
@@ -0,0 +1,38 @@
+/*
+ * 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.utils;
+
+/**
+ * This interface provides statistics on the current decompression stream.
+ * The stream consumer can use that statistics to handle abnormal
+ * compression ratios, i.e. to prevent zip bombs.
+ *
+ * @since 1.17
+ */
+public interface InputStreamStatistics {
+ /**
+ * @return the amount of raw or compressed bytes read by the stream
+ */
+ long getCompressedCount();
+
+ /**
+ * @return the amount of decompressed bytes returned by the stream
+ */
+ long getUncompressedCount();
+}
diff --git a/src/main/java/org/apache/commons/compress/utils/Iterators.java b/src/main/java/org/apache/commons/compress/utils/Iterators.java
new file mode 100644
index 0000000..0db0c36
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/utils/Iterators.java
@@ -0,0 +1,62 @@
+/*
+ * 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.utils;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Iterator utilities.
+ *
+ * @since 1.13.
+ */
+public class Iterators {
+
+ /**
+ * Adds all the elements in the source {@code iterator} to the target
+ * {@code collection}.
+ *
+ *
+ * When this method returns, the {@code iterator} will be "empty": its
+ * {@code hasNext()} method returns {@code false}.
+ * This is a lose port of MultiReadOnlySeekableByteChannel
+ * by Tim Underwood. This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception
+ * when invoked on a closed channel. Instead it will return the position the channel had when close has been
+ * called. When this channel is used for writing an internal buffer grows to accommodate incoming data. The natural size
+ * limit is the value of {@link Integer#MAX_VALUE} and it is not possible to {@link #position(long) set the position} or
+ * {@link #truncate truncate} to a value bigger than that. Internal buffer can be accessed via {@link
+ * SeekableInMemoryByteChannel#array()}. This constructor is intended to be used with pre-allocated buffer or when
+ * reading from a given byte array. Creates a channel and allocates internal storage of a given size. This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception
+ * when invoked on a closed channel. Instead it will return the position the channel had when close has been
+ * called. This method violates the contract of {@link SeekableByteChannel#size} as it will not throw any exception when
+ * invoked on a closed channel. Instead it will return the size the channel had when close has been called. This method violates the contract of {@link SeekableByteChannel#truncate} as it will not throw any exception when
+ * invoked on a closed channel. NOTE:
+ * The returned buffer is not aligned with containing data, use
+ * {@link #size()} to obtain the size of data stored in the buffer. Some implementations of {@link InputStream} implement {@link
+ * InputStream#skip} in a way that throws an exception if the stream
+ * is not seekable - {@link System#in System.in} is known to behave
+ * that way. For such a stream it is impossible to invoke skip at all
+ * and you have to read from the stream (and discard the data read)
+ * instead. Skipping is potentially much faster than reading so we do
+ * want to invoke {@code skip} when possible. We provide this class so
+ * you can wrap your own {@link InputStream} in it if you encounter
+ * problems with {@code skip} throwing an exception. Contains utilities used internally by the compress library.
+ *
+ */
+ public static final SevenZFileOptions DEFAULT = new SevenZFileOptions(DEFAUL_MEMORY_LIMIT_IN_KB,
+ DEFAULT_USE_DEFAULTNAME_FOR_UNNAMED_ENTRIES,
+ DEFAULT_TRY_TO_RECOVER_BROKEN_ARCHIVES);
+
+ /**
+ * Obtains a builder for SevenZFileOptions.
+ * @return a builder for SevenZFileOptions.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Gets the maximum amount of memory to use for parsing the
+ * archive and during extraction.
+ *
+ *
+ *
+ *
+ * @Immutable
+ * @since 1.8
+ */
+public class SevenZMethodConfiguration {
+ private final SevenZMethod method;
+ private final Object options;
+
+ /**
+ * Doesn't configure any additional options.
+ * @param method the method to use
+ */
+ public SevenZMethodConfiguration(final SevenZMethod method) {
+ this(method, null);
+ }
+
+ /**
+ * Specifies and method plus configuration options.
+ * @param method the method to use
+ * @param options the options to use
+ * @throws IllegalArgumentException if the method doesn't understand the options specified.
+ */
+ public SevenZMethodConfiguration(final SevenZMethod method, final Object options) {
+ this.method = method;
+ this.options = options;
+ if (options != null && !Coders.findByMethod(method).canAcceptOptions(options)) {
+ throw new IllegalArgumentException("The " + method + " method doesn't support options of type "
+ + options.getClass());
+ }
+ }
+
+ /**
+ * The specified method.
+ * @return the method
+ */
+ public SevenZMethod getMethod() {
+ return method;
+ }
+
+ /**
+ * The specified options.
+ * @return the options
+ */
+ public Object getOptions() {
+ return options;
+ }
+
+ @Override
+ public int hashCode() {
+ return method == null ? 0 : method.hashCode();
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ final SevenZMethodConfiguration other = (SevenZMethodConfiguration) obj;
+ return Objects.equals(method, other.method)
+ && Objects.equals(options, other.options);
+ }
+}
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
new file mode 100644
index 0000000..68cf90f
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/archivers/sevenz/SevenZOutputFile.java
@@ -0,0 +1,899 @@
+/*
+ * 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 static java.nio.charset.StandardCharsets.UTF_16LE;
+import static org.apache.commons.compress.utils.ByteUtils.utf16Decode;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.CRC32;
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.utils.CountingOutputStream;
+
+/**
+ * Writes a 7z file.
+ * @since 1.6
+ */
+public class SevenZOutputFile implements Closeable {
+
+ private final SeekableByteChannel channel;
+ private final List
+ * Method Option Type Description
+ * BZIP2 Number Block Size - an number between 1 and 9
+ * DEFLATE Number Compression Level - an number between 1 and 9
+ * LZMA2 Number Dictionary Size - a number between 4096 and 768 MiB (768 << 20)
+ * LZMA2 org.tukaani.xz.LZMA2Options Whole set of LZMA2 options.
+ * DELTA_FILTER Number Delta Distance - a number between 1 and 256
+ * - 2000 main.c
+ * d 100 testfiles
+ *
+ *
+ * @param entry the entry
+ * @return the representation of the entry
+ */
+ public static String toString(final ArchiveEntry entry){
+ final StringBuilder sb = new StringBuilder();
+ sb.append(entry.isDirectory()? 'd' : '-');// c.f. "ls -l" output
+ final String size = Long.toString(entry.getSize());
+ sb.append(' ');
+ // Pad output to 7 places, leading spaces
+ for(int i=7; i > size.length(); i--){
+ sb.append(' ');
+ }
+ sb.append(size);
+ sb.append(' ').append(entry.getName());
+ return sb.toString();
+ }
+
+ /**
+ * Check if buffer contents matches Ascii String.
+ *
+ * @param expected expected string
+ * @param buffer the buffer
+ * @param offset offset to read from
+ * @param length length of the buffer
+ * @return {@code true} if buffer is the same as the expected string
+ */
+ public static boolean matchAsciiBuffer(
+ final String expected, final byte[] buffer, final int offset, final int length){
+ final byte[] buffer1;
+ buffer1 = expected.getBytes(US_ASCII);
+ return isEqual(buffer1, 0, buffer1.length, buffer, offset, length, false);
+ }
+
+ /**
+ * Check if buffer contents matches Ascii String.
+ *
+ * @param expected the expected strin
+ * @param buffer the buffer
+ * @return {@code true} if buffer is the same as the expected string
+ */
+ public static boolean matchAsciiBuffer(final String expected, final byte[] buffer){
+ return matchAsciiBuffer(expected, buffer, 0, buffer.length);
+ }
+
+ /**
+ * Convert a string to Ascii bytes.
+ * Used for comparing "magic" strings which need to be independent of the default Locale.
+ *
+ * @param inputString string to convert
+ * @return the bytes
+ */
+ public static byte[] toAsciiBytes(final String inputString){
+ return inputString.getBytes(US_ASCII);
+ }
+
+ /**
+ * Convert an input byte array to a String using the ASCII character set.
+ *
+ * @param inputBytes bytes to convert
+ * @return the bytes, interpreted as an Ascii string
+ */
+ public static String toAsciiString(final byte[] inputBytes){
+ return new String(inputBytes, US_ASCII);
+ }
+
+ /**
+ * Convert an input byte array to a String using the ASCII character set.
+ *
+ * @param inputBytes input byte array
+ * @param offset offset within array
+ * @param length length of array
+ * @return the bytes, interpreted as an Ascii string
+ */
+ public static String toAsciiString(final byte[] inputBytes, final int offset, final int length){
+ return new String(inputBytes, offset, length, US_ASCII);
+ }
+
+ /**
+ * Compare byte buffers, optionally ignoring trailing nulls
+ *
+ * @param buffer1 first buffer
+ * @param offset1 first offset
+ * @param length1 first length
+ * @param buffer2 second buffer
+ * @param offset2 second offset
+ * @param length2 second length
+ * @param ignoreTrailingNulls whether to ignore trailing nulls
+ * @return {@code true} if buffer1 and buffer2 have same contents, having regard to trailing nulls
+ */
+ public static boolean isEqual(
+ final byte[] buffer1, final int offset1, final int length1,
+ final byte[] buffer2, final int offset2, final int length2,
+ final boolean ignoreTrailingNulls){
+ final int minLen= Math.min(length1, length2);
+ for (int i=0; i < minLen; i++){
+ if (buffer1[offset1+i] != buffer2[offset2+i]){
+ return false;
+ }
+ }
+ if (length1 == length2){
+ return true;
+ }
+ if (ignoreTrailingNulls){
+ if (length1 > length2){
+ for(int i = length2; i < length1; i++){
+ if (buffer1[offset1+i] != 0){
+ return false;
+ }
+ }
+ } else {
+ for(int i = length1; i < length2; i++){
+ if (buffer2[offset2+i] != 0){
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Compare byte buffers
+ *
+ * @param buffer1 the first buffer
+ * @param offset1 the first offset
+ * @param length1 the first length
+ * @param buffer2 the second buffer
+ * @param offset2 the second offset
+ * @param length2 the second length
+ * @return {@code true} if buffer1 and buffer2 have same contents
+ */
+ public static boolean isEqual(
+ final byte[] buffer1, final int offset1, final int length1,
+ final byte[] buffer2, final int offset2, final int length2){
+ return isEqual(buffer1, offset1, length1, buffer2, offset2, length2, false);
+ }
+
+ /**
+ * Compare byte buffers
+ *
+ * @param buffer1 the first buffer
+ * @param buffer2 the second buffer
+ * @return {@code true} if buffer1 and buffer2 have same contents
+ */
+ public static boolean isEqual(final byte[] buffer1, final byte[] buffer2 ){
+ return isEqual(buffer1, 0, buffer1.length, buffer2, 0, buffer2.length, false);
+ }
+
+ /**
+ * Compare byte buffers, optionally ignoring trailing nulls
+ *
+ * @param buffer1 the first buffer
+ * @param buffer2 the second buffer
+ * @param ignoreTrailingNulls whether to ignore trailing nulls
+ * @return {@code true} if buffer1 and buffer2 have same contents
+ */
+ public static boolean isEqual(final byte[] buffer1, final byte[] buffer2, final boolean ignoreTrailingNulls){
+ return isEqual(buffer1, 0, buffer1.length, buffer2, 0, buffer2.length, ignoreTrailingNulls);
+ }
+
+ /**
+ * Compare byte buffers, ignoring trailing nulls
+ *
+ * @param buffer1 the first buffer
+ * @param offset1 the first offset
+ * @param length1 the first length
+ * @param buffer2 the second buffer
+ * @param offset2 the second offset
+ * @param length2 the second length
+ * @return {@code true} if buffer1 and buffer2 have same contents, having regard to trailing nulls
+ */
+ public static boolean isEqualWithNull(
+ final byte[] buffer1, final int offset1, final int length1,
+ final byte[] buffer2, final int offset2, final int length2){
+ return isEqual(buffer1, offset1, length1, buffer2, offset2, length2, true);
+ }
+
+ /**
+ * Returns true if the first N bytes of an array are all zero
+ *
+ * @param a
+ * The array to check
+ * @param size
+ * The number of characters to check (not the size of the array)
+ * @return true if the first N bytes are zero
+ */
+ public static boolean isArrayZero(final byte[] a, final int size) {
+ for (int i = 0; i < size; i++) {
+ if (a[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns a "sanitized" version of the string given as arguments,
+ * where sanitized means non-printable characters have been
+ * replaced with a question mark and the outcome is not longer
+ * than 255 chars.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *