diff --git a/app/build.gradle b/app/build.gradle index 34d51be..a1ce9fc 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,15 +14,18 @@ version '1.0' group 'com.baker' android { - compileSdkVersion 21 - buildToolsVersion "21.1.2" + compileSdkVersion 22 + buildToolsVersion '22.0.1' defaultConfig { applicationId project.property("application-id") minSdkVersion 14 - targetSdkVersion 21 + targetSdkVersion 22 versionCode 10 versionName "1.0" + + // Enabled because google play services 7.5 pushes method limit over 65536 + multiDexEnabled true } buildTypes { @@ -36,10 +39,11 @@ android { } dexOptions { - incremental true javaMaxHeapSize "4g" preDexLibraries = false } + productFlavors { + } } // Release signing for debugging @@ -48,27 +52,30 @@ if(project.hasProperty("extra-gradle") && new File(project.property("extra-gradl } repositories { - maven { url "http://dl.bintray.com/populov/maven" } - maven { url "https://download.01.org/crosswalk/releases/crosswalk/android/maven2" } + maven { url 'http://dl.bintray.com/populov/maven' } + maven { url 'https://download.01.org/crosswalk/releases/crosswalk/android/maven2' } mavenCentral() - maven { - url 'file://' + new File(System.getProperty('user.home'), '.m2/repository').absolutePath - } } dependencies { - compile 'com.google.android.gms:play-services:6.5.+' + compile 'com.google.android.gms:play-services:7.5.0' compile 'org.jsoup:jsoup:1.7.3' compile 'org.apache.commons:commons-compress:1.8.1' compile 'com.viewpagerindicator:library:2.4.1@aar' - compile 'com.android.support:appcompat-v7:21.+' - compile 'org.solovyev.android:checkout:0.6.2@aar' + compile 'com.android.support:appcompat-v7:22.2.1' + compile 'org.solovyev.android:checkout:0.7.4@aar' compile 'com.google.code.gson:gson:2.3.1' compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3' compile 'com.path:android-priority-jobqueue:1.1.2' compile 'de.greenrobot:eventbus:2.4.0' - compile 'org.zeroturnaround:zt-zip:1.8' - compile 'com.parse.bolts:bolts-android:1.+' - compile 'org.xwalk:xwalk_core_library:11.40.277.7' + compile 'com.parse.bolts:bolts-android:1.2.1' + compile 'org.xwalk:xwalk_core_library:12.41.296.9' + + // Enabled because google play services 7.5 pushes method limit over 65536 + compile 'com.android.support:multidex:1.0.0' + + // org.zeroturnaround.zip requires this + compile 'org.slf4j:slf4j-api:1.7.12' + compile fileTree(dir: 'libs', include: 'Parse-*.jar') } diff --git a/app/src/main/java/com/bakerframework/baker/BakerApplication.java b/app/src/main/java/com/bakerframework/baker/BakerApplication.java index fff59ec..d141a99 100755 --- a/app/src/main/java/com/bakerframework/baker/BakerApplication.java +++ b/app/src/main/java/com/bakerframework/baker/BakerApplication.java @@ -35,6 +35,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.support.annotation.NonNull; +import android.support.multidex.MultiDexApplication; import android.util.Log; import com.bakerframework.baker.handler.PluginManager; @@ -57,7 +58,7 @@ import static org.solovyev.android.checkout.ProductTypes.IN_APP; import static org.solovyev.android.checkout.ProductTypes.SUBSCRIPTION; -public class BakerApplication extends Application { +public class BakerApplication extends MultiDexApplication { private static BakerApplication instance; private JobManager jobManager; private PluginManager pluginManager; diff --git a/app/src/main/java/com/bakerframework/baker/activity/ShelfActivity.java b/app/src/main/java/com/bakerframework/baker/activity/ShelfActivity.java index 108bb1c..c1799ac 100755 --- a/app/src/main/java/com/bakerframework/baker/activity/ShelfActivity.java +++ b/app/src/main/java/com/bakerframework/baker/activity/ShelfActivity.java @@ -32,13 +32,13 @@ import android.provider.Settings; import android.support.annotation.NonNull; import android.support.v4.widget.SwipeRefreshLayout; -import android.support.v7.app.ActionBarActivity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.Gravity; import android.view.Menu; @@ -89,7 +89,7 @@ import de.greenrobot.event.EventBus; -public class ShelfActivity extends ActionBarActivity implements SwipeRefreshLayout.OnRefreshListener { +public class ShelfActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener { public static final int STANDALONE_MAGAZINE_ACTIVITY_FINISH = 1; static final int SHELF_CHECKOUT_REQUEST_CODE = 0XCAFE; diff --git a/app/src/main/java/com/bakerframework/baker/settings/SettingsActivity.java b/app/src/main/java/com/bakerframework/baker/settings/SettingsActivity.java index 2849d90..0464d02 100755 --- a/app/src/main/java/com/bakerframework/baker/settings/SettingsActivity.java +++ b/app/src/main/java/com/bakerframework/baker/settings/SettingsActivity.java @@ -28,19 +28,19 @@ import android.app.ActionBar; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.AppCompatActivity; import com.bakerframework.baker.R; -public class SettingsActivity extends ActionBarActivity { +public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Modify the action bar to use a custom layout to center the title. - this.getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + this.getSupportActionBar().setDisplayOptions( ActionBar.DISPLAY_SHOW_CUSTOM ); this.getSupportActionBar().setCustomView(R.layout.settings_actionbar); getFragmentManager().beginTransaction() diff --git a/app/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java b/app/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java index e5b819a..17cc7a7 100644 --- a/app/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java +++ b/app/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java @@ -68,10 +68,10 @@ public class APKExpansionPolicy implements Policy { private long mRetryCount; private long mLastResponseTime = 0; private int mLastResponse; - private final PreferenceObfuscator mPreferences; - private final Vector mExpansionURLs = new Vector(); - private final Vector mExpansionFileNames = new Vector(); - private final Vector mExpansionFileSizes = new Vector(); + private PreferenceObfuscator mPreferences; + private Vector mExpansionURLs = new Vector(); + private Vector mExpansionFileNames = new Vector(); + private Vector mExpansionFileSizes = new Vector(); /** * The design of the protocol supports n files. Currently the market can @@ -127,7 +127,7 @@ public void resetPolicy() { * @param rawData the raw server response data */ public void processServerResponse(int response, - ResponseData rawData) { + com.google.android.vending.licensing.ResponseData rawData) { // Update retry counter if (response != Policy.RETRY) { diff --git a/app/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java b/app/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java index a9dd491..d90d6ea 100644 --- a/app/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java +++ b/app/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java @@ -9,12 +9,12 @@ import android.os.IInterface; import android.os.Binder; import android.os.Parcel; -public interface ILicenseResultListener extends IInterface +public interface ILicenseResultListener extends android.os.IInterface { /** Local-side IPC implementation stub class. */ -public static abstract class Stub extends Binder implements ILicenseResultListener +public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener { -private static final String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; +private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; /** Construct the stub at attach it to the interface. */ public Stub() { @@ -24,22 +24,22 @@ public Stub() * Cast an IBinder object into an ILicenseResultListener interface, * generating a proxy if needed. */ -public static ILicenseResultListener asInterface(IBinder obj) +public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) { if ((obj==null)) { return null; } -IInterface iin = (IInterface)obj.queryLocalInterface(DESCRIPTOR); -if (((iin!=null)&&(iin instanceof ILicenseResultListener))) { -return ((ILicenseResultListener)iin); +android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); +if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) { +return ((com.google.android.vending.licensing.ILicenseResultListener)iin); } -return new Proxy(obj); +return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj); } -public IBinder asBinder() +public android.os.IBinder asBinder() { return this; } -public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException +public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code) { @@ -53,9 +53,9 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws data.enforceInterface(DESCRIPTOR); int _arg0; _arg0 = data.readInt(); -String _arg1; +java.lang.String _arg1; _arg1 = data.readString(); -String _arg2; +java.lang.String _arg2; _arg2 = data.readString(); this.verifyLicense(_arg0, _arg1, _arg2); return true; @@ -63,24 +63,24 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws } return super.onTransact(code, data, reply, flags); } -private static class Proxy implements ILicenseResultListener +private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener { -private final IBinder mRemote; -Proxy(IBinder remote) +private android.os.IBinder mRemote; +Proxy(android.os.IBinder remote) { mRemote = remote; } -public IBinder asBinder() +public android.os.IBinder asBinder() { return mRemote; } -public String getInterfaceDescriptor() +public java.lang.String getInterfaceDescriptor() { return DESCRIPTOR; } -public void verifyLicense(int responseCode, String signedData, String signature) throws RemoteException +public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException { -Parcel _data = Parcel.obtain(); +android.os.Parcel _data = android.os.Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR); _data.writeInt(responseCode); @@ -95,5 +95,5 @@ public void verifyLicense(int responseCode, String signedData, String signature) } static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); } -public void verifyLicense(int responseCode, String signedData, String signature) throws RemoteException; +public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException; } diff --git a/app/src/main/java/com/google/android/vending/licensing/ILicensingService.java b/app/src/main/java/com/google/android/vending/licensing/ILicensingService.java index 9d25425..9559954 100644 --- a/app/src/main/java/com/google/android/vending/licensing/ILicensingService.java +++ b/app/src/main/java/com/google/android/vending/licensing/ILicensingService.java @@ -9,12 +9,12 @@ import android.os.IInterface; import android.os.Binder; import android.os.Parcel; -public interface ILicensingService extends IInterface +public interface ILicensingService extends android.os.IInterface { /** Local-side IPC implementation stub class. */ -public static abstract class Stub extends Binder implements ILicensingService +public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService { -private static final String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; +private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; /** Construct the stub at attach it to the interface. */ public Stub() { @@ -24,22 +24,22 @@ public Stub() * Cast an IBinder object into an ILicensingService interface, * generating a proxy if needed. */ -public static ILicensingService asInterface(IBinder obj) +public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) { if ((obj==null)) { return null; } -IInterface iin = (IInterface)obj.queryLocalInterface(DESCRIPTOR); -if (((iin!=null)&&(iin instanceof ILicensingService))) { -return ((ILicensingService)iin); +android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); +if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) { +return ((com.google.android.vending.licensing.ILicensingService)iin); } -return new Proxy(obj); +return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj); } -public IBinder asBinder() +public android.os.IBinder asBinder() { return this; } -public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException +public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code) { @@ -53,7 +53,7 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws data.enforceInterface(DESCRIPTOR); long _arg0; _arg0 = data.readLong(); -String _arg1; +java.lang.String _arg1; _arg1 = data.readString(); com.google.android.vending.licensing.ILicenseResultListener _arg2; _arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder()); @@ -63,24 +63,24 @@ public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws } return super.onTransact(code, data, reply, flags); } -private static class Proxy implements ILicensingService +private static class Proxy implements com.google.android.vending.licensing.ILicensingService { -private final IBinder mRemote; -Proxy(IBinder remote) +private android.os.IBinder mRemote; +Proxy(android.os.IBinder remote) { mRemote = remote; } -public IBinder asBinder() +public android.os.IBinder asBinder() { return mRemote; } -public String getInterfaceDescriptor() +public java.lang.String getInterfaceDescriptor() { return DESCRIPTOR; } -public void checkLicense(long nonce, String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws RemoteException +public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException { -Parcel _data = Parcel.obtain(); +android.os.Parcel _data = android.os.Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR); _data.writeLong(nonce); @@ -95,5 +95,5 @@ public void checkLicense(long nonce, String packageName, com.google.android.vend } static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); } -public void checkLicense(long nonce, String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws RemoteException; +public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException; } diff --git a/app/src/main/java/com/google/android/vending/licensing/LicenseChecker.java b/app/src/main/java/com/google/android/vending/licensing/LicenseChecker.java index 92bfc83..98bdc37 100644 --- a/app/src/main/java/com/google/android/vending/licensing/LicenseChecker.java +++ b/app/src/main/java/com/google/android/vending/licensing/LicenseChecker.java @@ -46,7 +46,7 @@ /** * Client library for Android Market license verifications. *

- * The LicenseChecker is configured via a {@link com.google.android.vending.licensing.Policy} which contains the + * The LicenseChecker is configured via a {@link Policy} which contains the * logic to determine whether a user should have access to the application. For * example, the Policy can define a threshold for allowable number of server or * client failures before the library reports the user as not having access. @@ -67,14 +67,14 @@ public class LicenseChecker implements ServiceConnection { private ILicensingService mService; - private final PublicKey mPublicKey; + private PublicKey mPublicKey; private final Context mContext; private final Policy mPolicy; /** * A handler for running tasks on a background thread. We don't want license * processing to block the UI thread. */ - private final Handler mHandler; + private Handler mHandler; private final String mPackageName; private final String mVersionCode; private final Set mChecksInProgress = new HashSet(); @@ -146,11 +146,16 @@ public synchronized void checkAccess(LicenseCheckerCallback callback) { if (mService == null) { Log.i(TAG, "Binding to licensing service."); try { + + Intent serviceIntent = new Intent( + new String(Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U=")) + ); + + serviceIntent.setPackage("com.android.vending"); + boolean bindResult = mContext .bindService( - new Intent( - new String( - Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))), + serviceIntent, this, // ServiceConnection. Context.BIND_AUTO_CREATE); @@ -197,7 +202,7 @@ private synchronized void finishCheck(LicenseValidator validator) { private class ResultListener extends ILicenseResultListener.Stub { private final LicenseValidator mValidator; - private final Runnable mOnTimeout; + private Runnable mOnTimeout; public ResultListener(LicenseValidator validator) { mValidator = validator; diff --git a/app/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java b/app/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java index 65095d0..b250a71 100644 --- a/app/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java +++ b/app/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -19,7 +19,7 @@ /** * Callback for the license checker library. *

- * Upon checking with the Market server and conferring with the {@link com.google.android.vending.licensing.Policy}, + * Upon checking with the Market server and conferring with the {@link Policy}, * the library calls the appropriate callback method to communicate the result. *

* The callback does not occur in the original checking thread. Your diff --git a/app/src/main/java/com/google/android/vending/licensing/Obfuscator.java b/app/src/main/java/com/google/android/vending/licensing/Obfuscator.java index b416430..b5d510d 100644 --- a/app/src/main/java/com/google/android/vending/licensing/Obfuscator.java +++ b/app/src/main/java/com/google/android/vending/licensing/Obfuscator.java @@ -17,7 +17,7 @@ package com.google.android.vending.licensing; /** - * Interface used as part of a {@link com.google.android.vending.licensing.Policy} to allow application authors to obfuscate + * Interface used as part of a {@link Policy} to allow application authors to obfuscate * licensing data that will be stored into a SharedPreferences file. *

* Any transformation scheme must be reversable. Implementing classes may optionally implement an diff --git a/app/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java b/app/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java index bb41c90..fbf8cf6 100644 --- a/app/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java +++ b/app/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java @@ -65,7 +65,7 @@ public class ServerManagedPolicy implements Policy { private long mRetryCount; private long mLastResponseTime = 0; private int mLastResponse; - private final PreferenceObfuscator mPreferences; + private PreferenceObfuscator mPreferences; /** * @param context The context for the current application diff --git a/app/src/main/java/com/google/android/vending/licensing/ValidationException.java b/app/src/main/java/com/google/android/vending/licensing/ValidationException.java index 2ff6134..ee4df47 100644 --- a/app/src/main/java/com/google/android/vending/licensing/ValidationException.java +++ b/app/src/main/java/com/google/android/vending/licensing/ValidationException.java @@ -18,7 +18,7 @@ /** * Indicates that an error occurred while validating the integrity of data managed by an - * {@link com.google.android.vending.licensing.Obfuscator}.} + * {@link Obfuscator}.} */ public class ValidationException extends Exception { public ValidationException() { diff --git a/app/src/main/java/org/zeroturnaround/zip/ByteSource.java b/app/src/main/java/org/zeroturnaround/zip/ByteSource.java new file mode 100644 index 0000000..90c1652 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ByteSource.java @@ -0,0 +1,89 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +public class ByteSource implements ZipEntrySource { + + private final String path; + private final byte[] bytes; + private final long time; + private final int compressionLevel; + private final long crc; + + public ByteSource(String path, byte[] bytes) { + this(path, bytes, System.currentTimeMillis()); + } + + public ByteSource(String path, byte[] bytes, long time) { + this(path, bytes, time, -1); + } + public ByteSource(String path, byte[] bytes, int compressionLevel) { + this(path, bytes, System.currentTimeMillis(), compressionLevel); + } + + public ByteSource(String path, byte[] bytes, long time, int compressionLevel) { + this.path = path; + this.bytes = (byte[])bytes.clone(); + this.time = time; + this.compressionLevel = compressionLevel; + if(compressionLevel != -1) { + CRC32 crc32 = new CRC32(); + crc32.update(bytes); + this.crc = crc32.getValue(); + } else { + this.crc = -1; + } + } + + public String getPath() { + return path; + } + + public ZipEntry getEntry() { + ZipEntry entry = new ZipEntry(path); + if (bytes != null) { + entry.setSize(bytes.length); + } + if(compressionLevel != -1) { + entry.setMethod(compressionLevel); + } + if(crc != -1L) { + entry.setCrc(crc); + } + entry.setTime(time); + return entry; + } + + public InputStream getInputStream() throws IOException { + if (bytes == null) { + return null; + } + else { + return new ByteArrayInputStream(bytes); + } + } + + public String toString() { + return "ByteSource[" + path + "]"; + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/FileSource.java b/app/src/main/java/org/zeroturnaround/zip/FileSource.java new file mode 100644 index 0000000..e034e26 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/FileSource.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; + +/** + * @author Toomas Romer + * @author shelajev + * @author Innokenty Shuvalov + */ +public class FileSource implements ZipEntrySource { + + private final String path; + private final File file; + + public FileSource(String path, File file) { + this.path = path; + this.file = file; + } + + public String getPath() { + return path; + } + + public ZipEntry getEntry() { + ZipEntry entry = ZipEntryUtil.fromFile(path, file); + return entry; + } + + public InputStream getInputStream() throws IOException { + if (file.isDirectory()) { + return null; + } + else { + return new BufferedInputStream(new FileInputStream(file)); + } + } + + public String toString() { + return "FileSource[" + path + ", " + file + "]"; + } + + /** + * Creates a sequence of FileSource objects via mapping + * a sequence of files to the sequence of corresponding names + * for the entries + * @param files file array to form the data of the objects + * in the resulting array + * @param names file array to form the names of the objects + * in the resulting array + * @return array of FileSource objects created by mapping + * given files array to the given names array one by one + * @throws java.lang.IllegalArgumentException if the names array + * contains less items than the files array + */ + public static FileSource[] pair(File[] files, String[] names) { + if (files.length > names.length) { + throw new IllegalArgumentException("names array must contain " + + "at least the same amount of items as files array or more"); + } + + FileSource[] result = new FileSource[files.length]; + for(int i = 0; i < files.length; i++) { + result[i] = new FileSource(names[i], files[i]); + } + return result; + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/IdentityNameMapper.java b/app/src/main/java/org/zeroturnaround/zip/IdentityNameMapper.java new file mode 100644 index 0000000..f65dd38 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/IdentityNameMapper.java @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +/** + * NOP implementation of the name mapper. + * + * @author Rein Raudjärv + * + * @see NameMapper + */ +final class IdentityNameMapper implements NameMapper { + + public static final NameMapper INSTANCE = new IdentityNameMapper(); + + private IdentityNameMapper() {} + + public String map(String name) { + return name; + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/Java6FileApiPermissionsStrategy.java b/app/src/main/java/org/zeroturnaround/zip/Java6FileApiPermissionsStrategy.java new file mode 100644 index 0000000..3e828d2 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/Java6FileApiPermissionsStrategy.java @@ -0,0 +1,69 @@ +package org.zeroturnaround.zip; + +import java.io.File; +import java.lang.reflect.Method; + +class Java6FileApiPermissionsStrategy implements ZTFilePermissionsStrategy { + private final Method canExecuteMethod; + private final Method setExecutableMethod; + private final Method setWritableMethod; + private final Method setReadableMethod; + + public Java6FileApiPermissionsStrategy() throws ZipException { + canExecuteMethod = ZTZipReflectionUtil.getDeclaredMethod(File.class, "canExecute"); + setExecutableMethod = ZTZipReflectionUtil.getDeclaredMethod(File.class, "setExecutable", boolean.class, boolean.class); + setReadableMethod = ZTZipReflectionUtil.getDeclaredMethod(File.class, "setReadable", boolean.class, boolean.class); + setWritableMethod = ZTZipReflectionUtil.getDeclaredMethod(File.class, "setWritable", boolean.class, boolean.class); + } + + public ZTFilePermissions getPermissions(File file) { + ZTFilePermissions permissions = new ZTFilePermissions(); + + permissions.setDirectory(file.isDirectory()); + + if (canExecute(file)) { + // set execute flag only for owner + permissions.setOwnerCanExecute(true); + } + + if (file.canWrite()) { + // 0644 for files and 0666 for directories + // this is a quite common choice for shared installations + permissions.setOwnerCanWrite(true); + if (file.isDirectory()) { + permissions.setGroupCanWrite(true); + permissions.setOthersCanWrite(true); + } + } + + if (file.canRead()) { + permissions.setOwnerCanRead(true); + permissions.setGroupCanRead(true); + permissions.setOthersCanRead(true); + } + + return permissions; + } + + public void setPermissions(File file, ZTFilePermissions permissions) { + setExecutable(file, permissions.isOwnerCanExecute(), !permissions.isGroupCanExecute() && !permissions.isOthersCanExecute()); + setWritable(file, permissions.isOwnerCanWrite(), !permissions.isGroupCanWrite() && !permissions.isOthersCanWrite()); + setReadable(file, permissions.isOwnerCanRead(), !permissions.isGroupCanRead() && !permissions.isOthersCanRead()); + } + + private boolean setExecutable(File file, boolean executable, boolean ownerOnly) { + return (Boolean) ZTZipReflectionUtil.invoke(setExecutableMethod, file, executable, ownerOnly); + } + + private boolean setWritable(File file, boolean executable, boolean ownerOnly) { + return (Boolean) ZTZipReflectionUtil.invoke(setWritableMethod, file, executable, ownerOnly); + } + + private boolean setReadable(File file, boolean executable, boolean ownerOnly) { + return (Boolean) ZTZipReflectionUtil.invoke(setReadableMethod, file, executable, ownerOnly); + } + + private boolean canExecute(File file) { + return (Boolean) ZTZipReflectionUtil.invoke(canExecuteMethod, file); + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/Java7Nio2ApiPermissionsStrategy.java b/app/src/main/java/org/zeroturnaround/zip/Java7Nio2ApiPermissionsStrategy.java new file mode 100644 index 0000000..d736306 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/Java7Nio2ApiPermissionsStrategy.java @@ -0,0 +1,147 @@ +package org.zeroturnaround.zip; + +import java.io.File; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +/** + * ZTFilePermissionsStrategy which uses Java 7 posix file permissions + * + * @author VIktor Karabut + */ +class Java7Nio2ApiPermissionsStrategy implements ZTFilePermissionsStrategy { + private final Class> posixFilePermissionClass; + private final Class filesClass; + private final Class pathClass; + private final Class> linkOptionClass; + private final Enum[] linkOptionsArray; + + private final Method toPathMethod; + private final Method setPosixFilePermissionsMethod; + private final Method getPosixFilePermissionsMethod; + + private final Object OWNER_READ; + private final Object OWNER_WRITE; + private final Object OWNER_EXECUTE; + private final Object GROUP_READ; + private final Object GROUP_WRITE; + private final Object GROUP_EXECUTE; + private final Object OTHERS_READ; + private final Object OTHERS_WRITE; + private final Object OTHERS_EXECUTE; + + + @SuppressWarnings("unchecked") + public Java7Nio2ApiPermissionsStrategy() { + if (!isPosix()) { + throw new ZipException("File system does not support POSIX file attributes"); + } + + posixFilePermissionClass = + (Class>) ZTZipReflectionUtil.getClassForName("java.nio.file.attribute.PosixFilePermission", Enum.class); + Enum[] constants = posixFilePermissionClass.getEnumConstants(); + OWNER_READ = constants[0]; + OWNER_WRITE = constants[1]; + OWNER_EXECUTE = constants[2]; + GROUP_READ = constants[3]; + GROUP_WRITE = constants[4]; + GROUP_EXECUTE = constants[5]; + OTHERS_READ = constants[6]; + OTHERS_WRITE = constants[7]; + OTHERS_EXECUTE = constants[8]; + + linkOptionClass = + (Class>) ZTZipReflectionUtil.getClassForName("java.nio.file.LinkOption", Enum.class); + linkOptionsArray = (Enum[]) Array.newInstance(linkOptionClass, 1); + linkOptionsArray[0] = (Enum) linkOptionClass.getEnumConstants()[0]; // LinkOption.NOFOLLOW_LINKS; + + filesClass = ZTZipReflectionUtil.getClassForName("java.nio.file.Files", Object.class); + pathClass = ZTZipReflectionUtil.getClassForName("java.nio.file.Path", Object.class); + + toPathMethod = ZTZipReflectionUtil.getDeclaredMethod(File.class, "toPath"); + setPosixFilePermissionsMethod = ZTZipReflectionUtil.getDeclaredMethod(filesClass, "setPosixFilePermissions", pathClass, Set.class); + getPosixFilePermissionsMethod = ZTZipReflectionUtil.getDeclaredMethod(filesClass, "getPosixFilePermissions", pathClass, linkOptionsArray.getClass()); + } + + public ZTFilePermissions getPermissions(File file) { + ZTFilePermissions permissions = new ZTFilePermissions(); + permissions.setDirectory(file.isDirectory()); + + Set posixFilePermissions = getPosixFilePermissions(file); + + permissions.setOwnerCanRead( posixFilePermissions.contains(OWNER_READ)); + permissions.setOwnerCanWrite( posixFilePermissions.contains(OWNER_WRITE)); + permissions.setOwnerCanExecute(posixFilePermissions.contains(OWNER_EXECUTE)); + + permissions.setGroupCanRead( posixFilePermissions.contains(GROUP_READ)); + permissions.setGroupCanWrite( posixFilePermissions.contains(GROUP_WRITE)); + permissions.setGroupCanExecute(posixFilePermissions.contains(GROUP_EXECUTE)); + + permissions.setOthersCanRead( posixFilePermissions.contains(OTHERS_READ)); + permissions.setOthersCanWrite( posixFilePermissions.contains(OTHERS_WRITE)); + permissions.setOthersCanExecute(posixFilePermissions.contains(OTHERS_EXECUTE)); + + return permissions; + } + + public void setPermissions(File file, ZTFilePermissions permissions) { + Set set = new HashSet(); + addIf(permissions.isOwnerCanRead(), set, OWNER_READ); + addIf(permissions.isOwnerCanRead(), set,OWNER_READ); + addIf(permissions.isOwnerCanWrite(), set,OWNER_WRITE); + addIf(permissions.isOwnerCanExecute(),set,OWNER_EXECUTE); + + addIf(permissions.isGroupCanRead(), set,GROUP_READ); + addIf(permissions.isGroupCanWrite(), set,GROUP_WRITE); + addIf(permissions.isGroupCanExecute(),set,GROUP_EXECUTE); + + addIf(permissions.isOthersCanRead(), set,OTHERS_READ); + addIf(permissions.isOthersCanWrite(), set,OTHERS_WRITE); + addIf(permissions.isOthersCanExecute(),set,OTHERS_EXECUTE); + + setPosixFilePermissions(file, set); + } + + private void addIf(boolean condition, Set set, E el) { + if (condition) { + set.add(el); + } + } + + /** + * Construct java.nio.file.Path object from abstract path. + * Invokes JDK7 file.toPath() method through reflection. + * + * @param file + * @return instance of java.nio.file.Path object + */ + private Object toPath(File file) { + return ZTZipReflectionUtil.invoke(toPathMethod, file); + } + + // Files.setPosicFilePermissions(file.toPath(), set); + private void setPosixFilePermissions(File file, Set set) { + ZTZipReflectionUtil.invoke(setPosixFilePermissionsMethod, null, toPath(file), set); + } + + // Files.getPosixFilePermissions(file.toPath(), new LinkOption[]{ LinkOption.NOFOLLOW_LINKS }); + private Set getPosixFilePermissions(File file) { + return (Set) ZTZipReflectionUtil.invoke(getPosixFilePermissionsMethod, null, toPath(file), linkOptionsArray); + } + + // FileSystems.getDefault().supportedFileAttrubuteViews().contains("posix"); + private static boolean isPosix() { + Method getDefaultMethod = ZTZipReflectionUtil.getDeclaredMethod( + ZTZipReflectionUtil.getClassForName("java.nio.file.FileSystems", Object.class), "getDefault"); + Method supportedFileAttributeViewsMethod = ZTZipReflectionUtil.getDeclaredMethod( + ZTZipReflectionUtil.getClassForName("java.nio.file.FileSystem", Object.class), "supportedFileAttributeViews"); + + Object fileSystem = ZTZipReflectionUtil.invoke(getDefaultMethod, null); + @SuppressWarnings("unchecked") + Set views = (Set) ZTZipReflectionUtil.invoke(supportedFileAttributeViewsMethod, fileSystem); + + return views.contains("posix"); + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/NameMapper.java b/app/src/main/java/org/zeroturnaround/zip/NameMapper.java new file mode 100644 index 0000000..58722b3 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/NameMapper.java @@ -0,0 +1,34 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +/** + * Call-back for filtering and renaming ZIP entries while packing or unpacking. + * + * @author Rein Raudjärv + * + * @see ZipUtil + */ +public interface NameMapper { + + /** + * @param name original name. + * @return name to be stored in the ZIP file or the destination directory, + * null means that the entry will be skipped. + */ + String map(String name); + +} \ No newline at end of file diff --git a/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissions.java b/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissions.java new file mode 100644 index 0000000..e6e0912 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissions.java @@ -0,0 +1,102 @@ +package org.zeroturnaround.zip; + +/** + * This class holds POSIX file permissions. + * + * @author Viktor Karabut + */ +class ZTFilePermissions { + private boolean isDirectory; + + private boolean ownerCanRead; + private boolean ownerCanWrite; + private boolean ownerCanExecute; + + private boolean groupCanRead; + private boolean groupCanWrite; + private boolean groupCanExecute; + + private boolean othersCanRead; + private boolean othersCanWrite; + private boolean othersCanExecute; + + boolean isDirectory() { + return isDirectory; + } + + void setDirectory(boolean isDirectory) { + this.isDirectory = isDirectory; + } + + boolean isOwnerCanRead() { + return ownerCanRead; + } + + void setOwnerCanRead(boolean ownerCanRead) { + this.ownerCanRead = ownerCanRead; + } + + boolean isOwnerCanWrite() { + return ownerCanWrite; + } + + void setOwnerCanWrite(boolean ownerCanWrite) { + this.ownerCanWrite = ownerCanWrite; + } + + boolean isOwnerCanExecute() { + return ownerCanExecute; + } + + void setOwnerCanExecute(boolean ownerCanExecute) { + this.ownerCanExecute = ownerCanExecute; + } + + boolean isGroupCanRead() { + return groupCanRead; + } + + void setGroupCanRead(boolean groupCanRead) { + this.groupCanRead = groupCanRead; + } + + boolean isGroupCanWrite() { + return groupCanWrite; + } + + void setGroupCanWrite(boolean groupCanWrite) { + this.groupCanWrite = groupCanWrite; + } + + boolean isGroupCanExecute() { + return groupCanExecute; + } + + void setGroupCanExecute(boolean groupCanExecute) { + this.groupCanExecute = groupCanExecute; + } + + boolean isOthersCanRead() { + return othersCanRead; + } + + void setOthersCanRead(boolean othersCanRead) { + this.othersCanRead = othersCanRead; + } + + boolean isOthersCanWrite() { + return othersCanWrite; + } + + void setOthersCanWrite(boolean othersCanWrite) { + this.othersCanWrite = othersCanWrite; + } + + boolean isOthersCanExecute() { + return othersCanExecute; + } + + void setOthersCanExecute(boolean othersCanExecute) { + this.othersCanExecute = othersCanExecute; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissionsStrategy.java b/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissionsStrategy.java new file mode 100644 index 0000000..4b71d26 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissionsStrategy.java @@ -0,0 +1,28 @@ +package org.zeroturnaround.zip; + +import java.io.File; + + +/** + * Maps {@link ZTFilePermissions} to real filesystem-specific file attributes. + * + * @author Viktor Karabut + */ +public interface ZTFilePermissionsStrategy { + + /** + * Get {@link ZTFilePermissions} from file. + * + * @param file file to get permissions for + * @return permissions or null if cannot retrieve permissions info by some reason. + */ + ZTFilePermissions getPermissions(File file); + + /** + * Set {@link ZTFilePermissions} to file + * + * @param file file to get permissions for + * @param permissions permission + */ + void setPermissions(File file, ZTFilePermissions permissions); +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissionsUtil.java b/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissionsUtil.java new file mode 100644 index 0000000..b21bb80 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZTFilePermissionsUtil.java @@ -0,0 +1,129 @@ +package org.zeroturnaround.zip; + +import java.io.File; + +/** + * Utilities to manipulate {@link ZTFilePermissions}. + * + * @author Viktor Karabut + */ +class ZTFilePermissionsUtil { + + private ZTFilePermissionsUtil() { + } + + private static final int OWNER_READ_FLAG = 0400; + private static final int OWNER_WRITE_FLAG = 0200; + private static final int OWNER_EXECUTE_FLAG = 0100; + + private static final int GROUP_READ_FLAG = 0040; + private static final int GROUP_WRITE_FLAG = 0020; + private static final int GROUP_EXECUTE_FLAG = 0010; + + private static final int OTHERS_READ_FLAG = 0004; + private static final int OTHERS_WRITE_FLAG = 0002; + private static final int OTHERS_EXECUTE_FLAG = 0001; + + /** + * Get most appropriate {@link ZTFilePermissionsStrategy} based on Java version and OS. + * + * @return + */ + static ZTFilePermissionsStrategy getDefaultStategy() { + return DEFAULT_STRATEGY; + } + + /** + * Convert {@link ZTFilePermissions} to POSIX file permission bit array. + * + * + * @param permissions permissions + * @return Posix mode + */ + static int toPosixFileMode(ZTFilePermissions permissions) { + int mode = 0; + + mode |= addFlag(permissions.isOwnerCanExecute(), OWNER_EXECUTE_FLAG); + mode |= addFlag(permissions.isGroupCanExecute(), GROUP_EXECUTE_FLAG); + mode |= addFlag(permissions.isOthersCanExecute(), OTHERS_EXECUTE_FLAG); + + mode |= addFlag(permissions.isOwnerCanWrite(), OWNER_WRITE_FLAG); + mode |= addFlag(permissions.isGroupCanWrite(), GROUP_WRITE_FLAG); + mode |= addFlag(permissions.isOthersCanWrite(), OTHERS_WRITE_FLAG); + + mode |= addFlag(permissions.isOwnerCanRead(), OWNER_READ_FLAG); + mode |= addFlag(permissions.isGroupCanRead(), GROUP_READ_FLAG); + mode |= addFlag(permissions.isOthersCanRead(), OTHERS_READ_FLAG); + + return mode; + } + + private static int addFlag(boolean condition, int flag) { + return condition ? flag : 0; + } + + /** + * Convert Posix mode to {@link ZTFilePermissions} + * + * @param mode + * @return + */ + static ZTFilePermissions fromPosixFileMode(int mode) { + ZTFilePermissions permissions = new ZTFilePermissions(); + + permissions.setOwnerCanExecute( (mode & OWNER_EXECUTE_FLAG) > 0 ); + permissions.setGroupCanExecute( (mode & GROUP_EXECUTE_FLAG) > 0 ); + permissions.setOthersCanExecute((mode & OTHERS_EXECUTE_FLAG) > 0 ); + + permissions.setOwnerCanWrite( (mode & OWNER_WRITE_FLAG) > 0 ); + permissions.setGroupCanWrite( (mode & GROUP_WRITE_FLAG) > 0 ); + permissions.setOthersCanWrite((mode & OTHERS_WRITE_FLAG) > 0 ); + + permissions.setOwnerCanRead( (mode & OWNER_READ_FLAG) > 0 ); + permissions.setGroupCanRead( (mode & GROUP_READ_FLAG) > 0 ); + permissions.setOthersCanRead((mode & OTHERS_READ_FLAG) > 0 ); + + return permissions; + } + + /** + * Empty {@link ZTFilePermissionsStrategy} implementation. + */ + private static final ZTFilePermissionsStrategy NOP_STRATEGY = new ZTFilePermissionsStrategy() { + public void setPermissions(File file, ZTFilePermissions permissions) { + // do nothing + } + + public ZTFilePermissions getPermissions(File file) { + // do nothing + return null; + } + }; + + private static final ZTFilePermissionsStrategy DEFAULT_STRATEGY = fetchDefaultStrategy(); + + private static ZTFilePermissionsStrategy fetchDefaultStrategy() { + ZTFilePermissionsStrategy strategy = tryInstantiateStrategy(Java7Nio2ApiPermissionsStrategy.class); + + if (strategy == null) { + strategy = tryInstantiateStrategy(Java6FileApiPermissionsStrategy.class); + } + + if (strategy == null) { + strategy = NOP_STRATEGY; + } + + return strategy; + } + + private static ZTFilePermissionsStrategy tryInstantiateStrategy(Class clazz) { + try { + return clazz.newInstance(); + } + catch (Exception e) { + // failed to instantiate strategy by some reason + return null; + } + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZTFileUtil.java b/app/src/main/java/org/zeroturnaround/zip/ZTFileUtil.java new file mode 100644 index 0000000..74aaa4c --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZTFileUtil.java @@ -0,0 +1,69 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.Collection; + +public final class ZTFileUtil { + private ZTFileUtil() { + } + + public static Collection listFiles(File dir) { + return listFiles(dir, null); + } + + public static Collection listFiles(File dir, FileFilter filter) { + Collection accumulator = new ArrayList(); + + if (dir.isFile()) { + return accumulator; + } + + if (filter == null) { + // Set default filter to accept any file + filter = new FileFilter() { + public boolean accept(File pathname) { + return true; + } + }; + } + + innerListFiles(dir, accumulator, filter); + return accumulator; + } + + private static void innerListFiles(File dir, Collection accumulator, FileFilter filter) { + + String[] filenames = dir.list(); + + if (filenames != null) { + for (int i = 0; i < filenames.length; i++) { + File file = new File(dir, filenames[i]); + if (file.isDirectory()) { + innerListFiles(file, accumulator, filter); + } + else { + if (filter != null && filter.accept(file)) { + accumulator.add(file); + } + } + } + } + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZTZipReflectionUtil.java b/app/src/main/java/org/zeroturnaround/zip/ZTZipReflectionUtil.java new file mode 100644 index 0000000..ea55012 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZTZipReflectionUtil.java @@ -0,0 +1,46 @@ +package org.zeroturnaround.zip; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class ZTZipReflectionUtil { + private ZTZipReflectionUtil() { + } + + static Class getClassForName(String name, Class clazz) { + try { + return Class.forName(name).asSubclass(clazz); + } + catch (ClassNotFoundException e) { + throw new ZipException(e); + } + catch (ClassCastException e) { + throw new ZipException(e); + } + } + + static Method getDeclaredMethod(Class clazz, String methodName, Class... parameterTypes) { + try { + return clazz.getDeclaredMethod(methodName, parameterTypes); + } + catch (NoSuchMethodException e) { + throw new ZipException(e); + } + } + + static Object invoke(Method method, Object obj, Object... args) throws ZipException { + try { + return method.invoke(obj, args); + } + catch (IllegalAccessException e) { + throw new ZipException(e); + } + catch (InvocationTargetException e) { + throw new ZipException(e); + } + catch (IllegalArgumentException e) { + throw new ZipException(e); + } + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipBreakException.java b/app/src/main/java/org/zeroturnaround/zip/ZipBreakException.java new file mode 100644 index 0000000..aefa17f --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipBreakException.java @@ -0,0 +1,15 @@ +package org.zeroturnaround.zip; + +public class ZipBreakException extends RuntimeException { + public ZipBreakException(String msg) { + super(msg); + } + + public ZipBreakException(Exception e) { + super(e); + } + + public ZipBreakException() { + super(); + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipEntryCallback.java b/app/src/main/java/org/zeroturnaround/zip/ZipEntryCallback.java new file mode 100644 index 0000000..82b4879 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipEntryCallback.java @@ -0,0 +1,40 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; + +/** + * Call-back for traversing ZIP entries with their contents. + * + * @author Rein Raudjärv + * + * @see ZipInfoCallback + */ +public interface ZipEntryCallback { + + /** + * Invoked for each entry in a ZIP file. + * + * @param in contents of the ZIP entry. + * @param zipEntry ZIP entry. + * @throws java.io.IOException when any processing exception occurs + */ + void process(InputStream in, ZipEntry zipEntry) throws IOException; + +} \ No newline at end of file diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipEntryOrInfoAdapter.java b/app/src/main/java/org/zeroturnaround/zip/ZipEntryOrInfoAdapter.java new file mode 100644 index 0000000..04f4987 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipEntryOrInfoAdapter.java @@ -0,0 +1,33 @@ +package org.zeroturnaround.zip; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; + +class ZipEntryOrInfoAdapter implements ZipEntryCallback, ZipInfoCallback { + + private final ZipEntryCallback entryCallback; + private final ZipInfoCallback infoCallback; + + public ZipEntryOrInfoAdapter(ZipEntryCallback entryCallback, ZipInfoCallback infoCallback) { + if (entryCallback != null && infoCallback != null || entryCallback == null && infoCallback == null) { + throw new IllegalArgumentException("Only one of ZipEntryCallback and ZipInfoCallback must be specified together"); + } + this.entryCallback = entryCallback; + this.infoCallback = infoCallback; + } + + public void process(ZipEntry zipEntry) throws IOException { + infoCallback.process(zipEntry); + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + if (entryCallback != null) { + entryCallback.process(in, zipEntry); + } + else { + process(zipEntry); + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipEntrySource.java b/app/src/main/java/org/zeroturnaround/zip/ZipEntrySource.java new file mode 100644 index 0000000..e07dfeb --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipEntrySource.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; + + +/** + * ZIP entry with its contents. + * + * @author Rein Raudjärv + */ +public interface ZipEntrySource { + + /** + * @return path of the given entry (not null). + */ + String getPath(); + + /** + * @return meta-data of the given entry (not null). + */ + ZipEntry getEntry(); + + /** + * @throws IOException can throw getting the InputStream + * @return an input stream of the given entry + * or null if this entry is a directory. + */ + InputStream getInputStream() throws IOException; + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipEntryUtil.java b/app/src/main/java/org/zeroturnaround/zip/ZipEntryUtil.java new file mode 100644 index 0000000..dd315da --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipEntryUtil.java @@ -0,0 +1,192 @@ +package org.zeroturnaround.zip; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.zeroturnaround.zip.commons.IOUtils; +import org.zeroturnaround.zip.extra.AsiExtraField; +import org.zeroturnaround.zip.extra.ExtraFieldUtils; +import org.zeroturnaround.zip.extra.ZipExtraField; + +/** + * Util class for static methods shared between ZipUtil and Zips. + * + * @author shelajev + * + */ +class ZipEntryUtil { + + private ZipEntryUtil() {} + + /** + * Copy entry + * + * @param original - zipEntry to copy + * @return copy of the original entry + */ + static ZipEntry copy(ZipEntry original) { + return copy(original, null); + } + + /** + * Copy entry with another name. + * + * @param original - zipEntry to copy + * @param newName - new entry name, optional, if null, ogirinal's entry + * @return copy of the original entry, but with the given name + */ + static ZipEntry copy(ZipEntry original, String newName) { + ZipEntry copy = new ZipEntry(newName == null ? original.getName() : newName); + if (original.getCrc() != -1) { + copy.setCrc(original.getCrc()); + } + if (original.getMethod() != -1) { + copy.setMethod(original.getMethod()); + } + if (original.getSize() >= 0) { + copy.setSize(original.getSize()); + } + if (original.getExtra() != null) { + copy.setExtra(original.getExtra()); + } + + copy.setComment(original.getComment()); + copy.setTime(original.getTime()); + return copy; + } + + /** + * Copies a given ZIP entry to a ZIP file. + * + * @param zipEntry + * a ZIP entry from existing ZIP file. + * @param in + * contents of the ZIP entry. + * @param out + * target ZIP stream. + */ + static void copyEntry(ZipEntry zipEntry, InputStream in, ZipOutputStream out) throws IOException { + copyEntry(zipEntry, in, out, true); + } + + /** + * Copies a given ZIP entry to a ZIP file. If this.preserveTimestamps is true, original timestamp + * is carried over, otherwise uses current time. + * + * @param zipEntry + * a ZIP entry from existing ZIP file. + * @param in + * contents of the ZIP entry. + * @param out + * target ZIP stream. + */ + static void copyEntry(ZipEntry zipEntry, InputStream in, ZipOutputStream out, boolean preserveTimestamps) throws IOException { + ZipEntry copy = copy(zipEntry); + copy.setTime(preserveTimestamps ? zipEntry.getTime() : System.currentTimeMillis()); + addEntry(copy, new BufferedInputStream(in), out); + } + + /** + * Adds a given ZIP entry to a ZIP file. + * + * @param zipEntry + * new ZIP entry. + * @param in + * contents of the ZIP entry. + * @param out + * target ZIP stream. + */ + static void addEntry(ZipEntry zipEntry, InputStream in, ZipOutputStream out) throws IOException { + out.putNextEntry(zipEntry); + if (in != null) { + IOUtils.copy(in, out); + } + out.closeEntry(); + } + + /** + * Create new Zip entry and fill it with associated with file meta-info + * + * @param name Zip entry name + * @param file source File + * @return newly created Zip entry + */ + static ZipEntry fromFile(String name, File file) { + ZipEntry zipEntry = new ZipEntry(name); + if (!file.isDirectory()) { + zipEntry.setSize(file.length()); + } + zipEntry.setTime(file.lastModified()); + + ZTFilePermissions permissions = ZTFilePermissionsUtil.getDefaultStategy().getPermissions(file); + if (permissions != null) { + ZipEntryUtil.setZTFilePermissions(zipEntry, permissions); + } + return zipEntry; + } + + + /** + * Add file permissions info to ZIP entry. + * Current implementation adds "ASi Unix" (tag 0x756e) extra block to entry. + * + * @param zipEntry ZIP entry + * @param permissions permissions to assign + */ + static boolean setZTFilePermissions(ZipEntry zipEntry, ZTFilePermissions permissions) { + try { + List fields = ExtraFieldUtils.parse(zipEntry.getExtra()); + AsiExtraField asiExtraField = getFirstAsiExtraField(fields); + if (asiExtraField == null) { + asiExtraField = new AsiExtraField(); + fields.add(asiExtraField); + } + + asiExtraField.setDirectory(zipEntry.isDirectory()); + asiExtraField.setMode(ZTFilePermissionsUtil.toPosixFileMode(permissions)); + zipEntry.setExtra(ExtraFieldUtils.mergeLocalFileDataData(fields)); + return true; + } + catch (java.util.zip.ZipException ze) { + return false; + } + } + + /** + * Get assigned to ZIP entry file permissions info. Current implementation tries to read "ASi Unix" (tag 0x756e) extra tag. + * "ASi Unix" + * @param zipEntry + * @return file permissions info or null if ZIP entry does not have "ASi Unix" extra field. + */ + static ZTFilePermissions getZTFilePermissions(ZipEntry zipEntry) { + try { + ZTFilePermissions permissions = null; + List fields = ExtraFieldUtils.parse(zipEntry.getExtra()); + AsiExtraField asiExtraField = getFirstAsiExtraField(fields); + if (asiExtraField != null) { + int mode = asiExtraField.getMode() & 0777; + permissions = ZTFilePermissionsUtil.fromPosixFileMode(mode); + } + return permissions; + } + catch (java.util.zip.ZipException ze) { + throw new ZipException(ze); + } + } + + private static AsiExtraField getFirstAsiExtraField(List fields) { + AsiExtraField asiExtraField = null; + for (ZipExtraField field : fields) { + if (field instanceof AsiExtraField) { + asiExtraField = (AsiExtraField) field; + } + } + return asiExtraField; + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipException.java b/app/src/main/java/org/zeroturnaround/zip/ZipException.java new file mode 100644 index 0000000..c33d450 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipException.java @@ -0,0 +1,15 @@ +package org.zeroturnaround.zip; + +public class ZipException extends RuntimeException { + public ZipException(String msg) { + super(msg); + } + + public ZipException(Exception e) { + super(e); + } + + public ZipException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipExceptionUtil.java b/app/src/main/java/org/zeroturnaround/zip/ZipExceptionUtil.java new file mode 100644 index 0000000..335dbe0 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipExceptionUtil.java @@ -0,0 +1,14 @@ +package org.zeroturnaround.zip; + +import java.io.IOException; + +class ZipExceptionUtil { + + /** + * Rethrow the given exception as a runtime exception. + */ + static ZipException rethrow(IOException e) { + throw new ZipException(e); + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipFileUtil.java b/app/src/main/java/org/zeroturnaround/zip/ZipFileUtil.java new file mode 100644 index 0000000..8ec4d5d --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipFileUtil.java @@ -0,0 +1,129 @@ +package org.zeroturnaround.zip; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.Charset; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * Mainly methods to lookup Zip* class constructors. This is needed + * because Java 6 doesn't have constructors with Charsets that were + * introduced in Java 7. + */ +class ZipFileUtil { + private static final String MISSING_METHOD_PLEASE_UPGRADE = "Your JRE doesn't support the ZipFile Charset constructor. Please upgrade JRE to 1.7 use this feature. Tried constructor ZipFile(File, Charset)."; + private static final String CONSTRUCTOR_MESSAGE_FOR_ZIPFILE = "Using constructor ZipFile(File, Charset) has failed: "; + private static final String CONSTRUCTOR_MESSAGE_FOR_OUTPUT = "Using constructor ZipOutputStream(OutputStream, Charset) has failed: "; + private static final String CONSTRUCTOR_MESSAGE_FOR_INPUT = "Using constructor ZipInputStream(InputStream, Charset) has failed: "; + + // Private constructor for the utility class + private ZipFileUtil() { + } + + /** + * Returns a ZipInputStream opened with a given charset. + */ + static ZipInputStream createZipInputStream(InputStream inStream, Charset charset) { + if (charset == null) + return new ZipInputStream(inStream); + + try { + Constructor constructor = ZipInputStream.class.getConstructor(new Class[] { InputStream.class, Charset.class }); + return (ZipInputStream) constructor.newInstance(new Object[] { inStream, charset }); + } + catch (NoSuchMethodException e) { + throw new IllegalStateException(MISSING_METHOD_PLEASE_UPGRADE, e); + } + catch (InstantiationException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_INPUT + e.getMessage(), e); + } + catch (IllegalAccessException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_INPUT + e.getMessage(), e); + } + catch (IllegalArgumentException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_INPUT + e.getMessage(), e); + } + catch (InvocationTargetException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_INPUT + e.getMessage(), e); + } + } + + + /** + * Returns a ZipOutputStream opened with a given charset. + */ + static ZipOutputStream createZipOutputStream(BufferedOutputStream outStream, Charset charset) { + if (charset == null) + return new ZipOutputStream(outStream); + + try { + Constructor constructor = ZipOutputStream.class.getConstructor(new Class[] { OutputStream.class, Charset.class }); + return (ZipOutputStream) constructor.newInstance(new Object[] { outStream, charset }); + } + catch (NoSuchMethodException e) { + throw new IllegalStateException(MISSING_METHOD_PLEASE_UPGRADE, e); + } + catch (InstantiationException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_OUTPUT + e.getMessage(), e); + } + catch (IllegalAccessException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_OUTPUT + e.getMessage(), e); + } + catch (IllegalArgumentException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_OUTPUT + e.getMessage(), e); + } + catch (InvocationTargetException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_OUTPUT + e.getMessage(), e); + } + } + + /** + * Returns a zipFile opened with a given charset + */ + static ZipFile getZipFile(File src, Charset charset) throws IOException { + if (charset == null) { + return new ZipFile(src); + } + + try { + Constructor constructor = ZipFile.class.getConstructor(new Class[] { File.class, Charset.class }); + return (ZipFile) constructor.newInstance(new Object[] { src, charset }); + } + catch (NoSuchMethodException e) { + throw new IllegalStateException(MISSING_METHOD_PLEASE_UPGRADE, e); + } + catch (InstantiationException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_ZIPFILE + e.getMessage(), e); + } + catch (IllegalAccessException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_ZIPFILE + e.getMessage(), e); + } + catch (IllegalArgumentException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_ZIPFILE + e.getMessage(), e); + } + catch (InvocationTargetException e) { + throw new IllegalStateException(CONSTRUCTOR_MESSAGE_FOR_ZIPFILE + e.getMessage(), e); + } + } + + /** + * Returns true if charsets are supported in this JRE. + */ + static boolean isCharsetSupported() throws IOException { + try { + ZipFile.class.getConstructor(new Class[] { File.class, Charset.class }); + return true; + } + catch (NoSuchMethodException e) { + return false; + } + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipInfoCallback.java b/app/src/main/java/org/zeroturnaround/zip/ZipInfoCallback.java new file mode 100644 index 0000000..5acb05e --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipInfoCallback.java @@ -0,0 +1,39 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.IOException; +import java.util.zip.ZipEntry; + +/** + * Call-back for traversing ZIP entries without their contents. + * + * @author Rein Raudjärv + * + * @see ZipEntryCallback + */ +public interface ZipInfoCallback { + + /** + * Invoked for each entry in a ZIP file. + * + * @param zipEntry ZIP entry. + * + * @throws java.io.IOException when any processing exception occurs + */ + void process(ZipEntry zipEntry) throws IOException; + +} \ No newline at end of file diff --git a/app/src/main/java/org/zeroturnaround/zip/ZipUtil.java b/app/src/main/java/org/zeroturnaround/zip/ZipUtil.java new file mode 100644 index 0000000..2d8ae25 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/ZipUtil.java @@ -0,0 +1,2812 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.Deflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.zeroturnaround.zip.commons.FileUtils; +import org.zeroturnaround.zip.commons.FilenameUtils; +import org.zeroturnaround.zip.commons.IOUtils; +import org.zeroturnaround.zip.transform.ZipEntryTransformer; +import org.zeroturnaround.zip.transform.ZipEntryTransformerEntry; + +/** + * ZIP file manipulation utilities. + * + * @author Rein Raudjärv + * @author Innokenty Shuvalov + * + * @see #containsEntry(File, String) + * @see #unpackEntry(File, String) + * @see #unpack(File, File) + * @see #pack(File, File) + */ +public final class ZipUtil { + + private static final String PATH_SEPARATOR = "/"; + + /** Default compression level */ + public static final int DEFAULT_COMPRESSION_LEVEL = Deflater.DEFAULT_COMPRESSION; + + // Use / instead of . to work around an issue with Maven Shade Plugin + private static final Logger log = LoggerFactory.getLogger("org/zeroturnaround/zip/ZipUtil".replace('/', '.')); // NOSONAR + + private ZipUtil() { + } + + /* Extracting single entries from ZIP files. */ + + /** + * Checks if the ZIP file contains the given entry. + * + * @param zip + * ZIP file. + * @param name + * entry name. + * @return true if the ZIP file contains the given entry. + */ + public static boolean containsEntry(File zip, String name) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + return zf.getEntry(name) != null; + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Returns the compression level of a given entry of the ZIP file. + * + * @param zip + * ZIP file. + * @param name + * entry name. + * @return Returns ZipEntry.STORED, ZipEntry.DEFLATED or -1 if + * the ZIP file does not contain the given entry. + */ + public static int getCompressionLevelOfEntry(File zip, String name) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + ZipEntry zipEntry = zf.getEntry(name); + if(zipEntry == null) { + return -1; + } + return zipEntry.getMethod(); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Checks if the ZIP file contains any of the given entries. + * + * @param zip + * ZIP file. + * @param names + * entry names. + * @return true if the ZIP file contains any of the given + * entries. + */ + public static boolean containsAnyEntry(File zip, String[] names) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + for (int i = 0; i < names.length; i++) { + if (zf.getEntry(names[i]) != null) { + return true; + } + } + return false; + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Unpacks a single entry from a ZIP file. + * + * @param zip + * ZIP file. + * @param name + * entry name. + * @return contents of the entry or null if it was not found. + */ + public static byte[] unpackEntry(File zip, String name) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + return doUnpackEntry(zf, name); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Unpacks a single entry from a ZIP file. + * + * @param zf + * ZIP file. + * @param name + * entry name. + * @return contents of the entry or null if it was not found. + */ + public static byte[] unpackEntry(ZipFile zf, String name) { + try { + return doUnpackEntry(zf, name); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * Unpacks a single entry from a ZIP file. + * + * @param zf + * ZIP file. + * @param name + * entry name. + * @return contents of the entry or null if it was not found. + */ + private static byte[] doUnpackEntry(ZipFile zf, String name) throws IOException { + ZipEntry ze = zf.getEntry(name); + if (ze == null) { + return null; // entry not found + } + + InputStream is = zf.getInputStream(ze); + try { + return IOUtils.toByteArray(is); + } + finally { + IOUtils.closeQuietly(is); + } + } + + /** + * Unpacks a single entry from a ZIP stream. + * + * @param is + * ZIP stream. + * @param name + * entry name. + * @return contents of the entry or null if it was not found. + */ + public static byte[] unpackEntry(InputStream is, String name) { + ByteArrayUnpacker action = new ByteArrayUnpacker(); + if (!handle(is, name, action)) + return null; // entry not found + return action.getBytes(); + } + + /** + * Copies an entry into a byte array. + * + * @author Rein Raudjärv + */ + private static class ByteArrayUnpacker implements ZipEntryCallback { + + private byte[] bytes; + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + bytes = IOUtils.toByteArray(in); + } + + public byte[] getBytes() { + return bytes; + } + + } + + /** + * Unpacks a single file from a ZIP archive to a file. + * + * @param zip + * ZIP file. + * @param name + * entry name. + * @param file + * target file to be created or overwritten. + * @return true if the entry was found and unpacked, + * false if the entry was not found. + */ + public static boolean unpackEntry(File zip, String name, File file) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + return doUnpackEntry(zf, name, file); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Unpacks a single file from a ZIP archive to a file. + * + * @param zf + * ZIP file. + * @param name + * entry name. + * @param file + * target file to be created or overwritten. + * @return true if the entry was found and unpacked, + * false if the entry was not found. + */ + public static boolean unpackEntry(ZipFile zf, String name, File file) { + try { + return doUnpackEntry(zf, name, file); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * Unpacks a single file from a ZIP archive to a file. + * + * @param zf + * ZIP file. + * @param name + * entry name. + * @param file + * target file to be created or overwritten. + * @return true if the entry was found and unpacked, + * false if the entry was not found. + */ + private static boolean doUnpackEntry(ZipFile zf, String name, File file) throws IOException { + if (log.isTraceEnabled()) { + log.trace("Extracting '" + zf.getName() + "' entry '" + name + "' into '" + file + "'."); + } + + ZipEntry ze = zf.getEntry(name); + if (ze == null) { + return false; // entry not found + } + + if(ze.isDirectory() || zf.getInputStream(ze) == null) { + if(file.isDirectory()) { + return true; + } + if(file.exists()) { + FileUtils.forceDelete(file); + } + return file.mkdirs(); + } + + InputStream in = new BufferedInputStream(zf.getInputStream(ze)); + try { + FileUtils.copy(in, file); + } + finally { + IOUtils.closeQuietly(in); + } + return true; + } + + /** + * Unpacks a single file from a ZIP stream to a file. + * + * @param is + * ZIP stream. + * @param name + * entry name. + * @param file + * target file to be created or overwritten. + * @return true if the entry was found and unpacked, + * false if the entry was not found. + * @throws java.io.IOException if file is not found or writing to it fails + */ + public static boolean unpackEntry(InputStream is, String name, File file) throws IOException { + return handle(is, name, new FileUnpacker(file)); + } + + /** + * Copies an entry into a File. + * + * @author Rein Raudjärv + */ + private static class FileUnpacker implements ZipEntryCallback { + + private final File file; + + public FileUnpacker(File file) { + this.file = file; + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + FileUtils.copy(in, file); + } + + } + + /* Traversing ZIP files */ + + /** + * Reads the given ZIP file and executes the given action for each entry. + *

+ * For each entry the corresponding input stream is also passed to the action. If you want to stop the loop + * then throw a ZipBreakException. + * + * @param zip + * input ZIP file. + * @param action + * action to be called for each entry. + * + * @see ZipEntryCallback + * @see #iterate(File, ZipInfoCallback) + */ + public static void iterate(File zip, ZipEntryCallback action) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + + Enumeration en = zf.entries(); + while (en.hasMoreElements()) { + ZipEntry e = (ZipEntry) en.nextElement(); + + InputStream is = zf.getInputStream(e); + try { + action.process(is, e); + } + catch (IOException ze) { + throw new ZipException("Failed to process zip entry '" + e.getName() + "' with action " + action, ze); + } + catch (ZipBreakException ex) { + break; + } + finally { + IOUtils.closeQuietly(is); + } + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Reads the given ZIP file and executes the given action for each given entry. + *

+ * For each given entry the corresponding input stream is also passed to the action. If you want to stop the loop then throw a ZipBreakException. + * + * @param zip + * input ZIP file. + * @param entryNames + * names of entries to iterate + * @param action + * action to be called for each entry. + * + * @see ZipEntryCallback + * @see #iterate(File, String[], ZipInfoCallback) + */ + public static void iterate(File zip, String[] entryNames, ZipEntryCallback action) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + + for (int i = 0; i < entryNames.length; i++) { + ZipEntry e = zf.getEntry(entryNames[i]); + if (e == null) { + continue; + } + InputStream is = zf.getInputStream(e); + try { + action.process(is, e); + } + catch (IOException ze) { + throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); + } + catch (ZipBreakException ex) { + break; + } + finally { + IOUtils.closeQuietly(is); + } + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Scans the given ZIP file and executes the given action for each entry. + *

+ * Only the meta-data without the actual data is read. If you want to stop the loop + * then throw a ZipBreakException. + * + * @param zip + * input ZIP file. + * @param action + * action to be called for each entry. + * + * @see ZipInfoCallback + * @see #iterate(File, ZipEntryCallback) + */ + public static void iterate(File zip, ZipInfoCallback action) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + + Enumeration en = zf.entries(); + while (en.hasMoreElements()) { + ZipEntry e = (ZipEntry) en.nextElement(); + try { + action.process(e); + } + catch (IOException ze) { + throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); + } + catch (ZipBreakException ex) { + break; + } + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Scans the given ZIP file and executes the given action for each given entry. + *

+ * Only the meta-data without the actual data is read. If you want to stop the loop then throw a ZipBreakException. + * + * @param zip + * input ZIP file. + * @param entryNames + * names of entries to iterate + * @param action + * action to be called for each entry. + * + * @see ZipInfoCallback + * @see #iterate(File, String[], ZipEntryCallback) + */ + public static void iterate(File zip, String[] entryNames, ZipInfoCallback action) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + + for (int i = 0; i < entryNames.length; i++) { + ZipEntry e = zf.getEntry(entryNames[i]); + if (e == null) { + continue; + } + try { + action.process(e); + } + catch (IOException ze) { + throw new ZipException("Failed to process zip entry '" + e.getName() + " with action " + action, ze); + } + catch (ZipBreakException ex) { + break; + } + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Reads the given ZIP stream and executes the given action for each entry. + *

+ * For each entry the corresponding input stream is also passed to the action. If you want to stop the loop + * then throw a ZipBreakException. + * + * @param is + * input ZIP stream (it will not be closed automatically). + * @param action + * action to be called for each entry. + * @param charset + * charset to process entries in + * + * @see ZipEntryCallback + * @see #iterate(File, ZipEntryCallback) + */ + public static void iterate(InputStream is, ZipEntryCallback action, Charset charset) { + try { + ZipInputStream in = null; + if (charset == null) { + in = new ZipInputStream(new BufferedInputStream(is)); + } + else { + in = ZipFileUtil.createZipInputStream(is, charset); + } + ZipEntry entry; + while ((entry = in.getNextEntry()) != null) { + try { + action.process(in, entry); + } + catch (IOException ze) { + throw new ZipException("Failed to process zip entry '" + entry.getName() + " with action " + action, ze); + } + catch (ZipBreakException ex) { + break; + } + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * See {@link #iterate(InputStream, ZipEntryCallback, Charset)}. This method + * is a shorthand for a version where no Charset is specified. + * + * @param is + * input ZIP stream (it will not be closed automatically). + * @param action + * action to be called for each entry. + * + * @see ZipEntryCallback + * @see #iterate(File, ZipEntryCallback) + */ + public static void iterate(InputStream is, ZipEntryCallback action) { + iterate(is, action, null); + } + + /** + * Reads the given ZIP stream and executes the given action for each given entry. + *

+ * For each given entry the corresponding input stream is also passed to the action. If you want to stop the loop then throw a ZipBreakException. + * + * @param is + * input ZIP stream (it will not be closed automatically). + * @param entryNames + * names of entries to iterate + * @param action + * action to be called for each entry. + * @param charset + * charset to process entries in + * + * @see ZipEntryCallback + * @see #iterate(File, String[], ZipEntryCallback) + */ + public static void iterate(InputStream is, String[] entryNames, ZipEntryCallback action, Charset charset) { + Set namesSet = new HashSet(); + for (int i = 0; i < entryNames.length; i++) { + namesSet.add(entryNames[i]); + } + try { + ZipInputStream in = null; + if (charset == null) { + in = new ZipInputStream(new BufferedInputStream(is)); + } + else { + in = ZipFileUtil.createZipInputStream(is, charset); + } + ZipEntry entry; + while ((entry = in.getNextEntry()) != null) { + if (!namesSet.contains(entry.getName())) { + // skip the unnecessary entry + continue; + } + try { + action.process(in, entry); + } + catch (IOException ze) { + throw new ZipException("Failed to process zip entry '" + entry.getName() + " with action " + action, ze); + } + catch (ZipBreakException ex) { + break; + } + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * See @link{ {@link #iterate(InputStream, ZipEntryCallback, Charset)}. It is a + * shorthand where no Charset is specified. + * + * @param is + * input ZIP stream (it will not be closed automatically). + * @param entryNames + * names of entries to iterate + * @param action + * action to be called for each entry. + * + * @see ZipEntryCallback + * @see #iterate(File, String[], ZipEntryCallback) + */ + public static void iterate(InputStream is, String[] entryNames, ZipEntryCallback action) { + iterate(is, entryNames, action, null); + } + + /** + * Reads the given ZIP file and executes the given action for a single entry. + * + * @param zip + * input ZIP file. + * @param name + * entry name. + * @param action + * action to be called for this entry. + * @return true if the entry was found, false if the + * entry was not found. + * + * @see ZipEntryCallback + */ + public static boolean handle(File zip, String name, ZipEntryCallback action) { + ZipFile zf = null; + try { + zf = new ZipFile(zip); + + ZipEntry ze = zf.getEntry(name); + if (ze == null) { + return false; // entry not found + } + + InputStream in = new BufferedInputStream(zf.getInputStream(ze)); + try { + action.process(in, ze); + } + finally { + IOUtils.closeQuietly(in); + } + return true; + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + } + + /** + * Reads the given ZIP stream and executes the given action for a single + * entry. + * + * @param is + * input ZIP stream (it will not be closed automatically). + * @param name + * entry name. + * @param action + * action to be called for this entry. + * @return true if the entry was found, false if the + * entry was not found. + * + * @see ZipEntryCallback + */ + public static boolean handle(InputStream is, String name, ZipEntryCallback action) { + SingleZipEntryCallback helper = new SingleZipEntryCallback(name, action); + iterate(is, helper); + return helper.found(); + } + + /** + * ZipEntryCallback which is only applied to single entry. + * + * @author Rein Raudjärv + */ + private static class SingleZipEntryCallback implements ZipEntryCallback { + + private final String name; + + private final ZipEntryCallback action; + + private boolean found; + + public SingleZipEntryCallback(String name, ZipEntryCallback action) { + this.name = name; + this.action = action; + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + if (name.equals(zipEntry.getName())) { + found = true; + action.process(in, zipEntry); + } + } + + public boolean found() { + return found; + } + + } + + /* Extracting whole ZIP files. */ + + /** + * Unpacks a ZIP file to the given directory. + *

+ * The output directory must not be a file. + * + * @param zip + * input ZIP file. + * @param outputDir + * output directory (created automatically if not found). + */ + public static void unpack(File zip, final File outputDir) { + unpack(zip, outputDir, IdentityNameMapper.INSTANCE); + } + + /** + * Unpacks a ZIP file to the given directory. + *

+ * The output directory must not be a file. + * + * @param zip + * input ZIP file. + * @param outputDir + * output directory (created automatically if not found). + * @param mapper + * call-back for renaming the entries. + */ + public static void unpack(File zip, File outputDir, NameMapper mapper) { + log.debug("Extracting '{}' into '{}'.", zip, outputDir); + iterate(zip, new Unpacker(outputDir, mapper)); + } + + /** + * Unwraps a ZIP file to the given directory shaving of root dir. + * If there are multiple root dirs or entries in the root of zip, + * ZipException is thrown. + *

+ * The output directory must not be a file. + * + * @param zip + * input ZIP file. + * @param outputDir + * output directory (created automatically if not found). + */ + public static void unwrap(File zip, final File outputDir) { + unwrap(zip, outputDir, IdentityNameMapper.INSTANCE); + } + + /** + * Unwraps a ZIP file to the given directory shaving of root dir. + * If there are multiple root dirs or entries in the root of zip, + * ZipException is thrown. + *

+ * The output directory must not be a file. + * + * @param zip + * input ZIP file. + * @param outputDir + * output directory (created automatically if not found). + * @param mapper + * call-back for renaming the entries. + */ + public static void unwrap(File zip, File outputDir, NameMapper mapper) { + log.debug("Unwrapping '{}' into '{}'.", zip, outputDir); + iterate(zip, new Unwraper(outputDir, mapper)); + } + + /** + * Unpacks a ZIP stream to the given directory. + *

+ * The output directory must not be a file. + * + * @param is + * inputstream for ZIP file. + * @param outputDir + * output directory (created automatically if not found). + */ + public static void unpack(InputStream is, File outputDir) { + unpack(is, outputDir, IdentityNameMapper.INSTANCE); + } + + /** + * Unpacks a ZIP stream to the given directory. + *

+ * The output directory must not be a file. + * + * @param is + * inputstream for ZIP file. + * @param outputDir + * output directory (created automatically if not found). + * @param mapper + * call-back for renaming the entries. + */ + public static void unpack(InputStream is, File outputDir, NameMapper mapper) { + log.debug("Extracting {} into '{}'.", is, outputDir); + iterate(is, new Unpacker(outputDir, mapper)); + } + + /** + * Unwraps a ZIP file to the given directory shaving of root dir. + * If there are multiple root dirs or entries in the root of zip, + * ZipException is thrown. + *

+ * The output directory must not be a file. + * + * @param is + * inputstream for ZIP file. + * @param outputDir + * output directory (created automatically if not found). + */ + public static void unwrap(InputStream is, File outputDir) { + unwrap(is, outputDir, IdentityNameMapper.INSTANCE); + } + + /** + * Unwraps a ZIP file to the given directory shaving of root dir. + * If there are multiple root dirs or entries in the root of zip, + * ZipException is thrown. + *

+ * The output directory must not be a file. + * + * @param is + * inputstream for ZIP file. + * @param outputDir + * output directory (created automatically if not found). + * @param mapper + * call-back for renaming the entries. + */ + public static void unwrap(InputStream is, File outputDir, NameMapper mapper) { + log.debug("Unwrapping {} into '{}'.", is, outputDir); + iterate(is, new Unwraper(outputDir, mapper)); + } + + /** + * Unpacks each ZIP entry. + * + * @author Rein Raudjärv + */ + private static class Unpacker implements ZipEntryCallback { + + private final File outputDir; + private final NameMapper mapper; + + public Unpacker(File outputDir, NameMapper mapper) { + this.outputDir = outputDir; + this.mapper = mapper; + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + String name = mapper.map(zipEntry.getName()); + if (name != null) { + File file = new File(outputDir, name); + if (zipEntry.isDirectory()) { + FileUtils.forceMkdir(file); + } + else { + FileUtils.forceMkdir(file.getParentFile()); + + if (log.isDebugEnabled() && file.exists()) { + log.debug("Overwriting file '{}'.", zipEntry.getName()); + } + + FileUtils.copy(in, file); + } + + ZTFilePermissions permissions = ZipEntryUtil.getZTFilePermissions(zipEntry); + if (permissions != null) { + ZTFilePermissionsUtil.getDefaultStategy().setPermissions(file, permissions); + } + } + } + } + + /** + * Unwraps entries excluding a single parent dir. If there are multiple roots + * ZipException is thrown. + * + * @author Oleg Shelajev + */ + private static class Unwraper implements ZipEntryCallback { + + private final File outputDir; + private final NameMapper mapper; + private String rootDir; + + public Unwraper(File outputDir, NameMapper mapper) { + this.outputDir = outputDir; + this.mapper = mapper; + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + String root = getRootName(zipEntry.getName()); + if (rootDir == null) { + rootDir = root; + } + else if (!rootDir.equals(root)) { + throw new ZipException("Unwrapping with multiple roots is not supported, roots: " + rootDir + ", " + root); + } + + String name = mapper.map(getUnrootedName(root, zipEntry.getName())); + if (name != null) { + File file = new File(outputDir, name); + if (zipEntry.isDirectory()) { + FileUtils.forceMkdir(file); + } + else { + FileUtils.forceMkdir(file.getParentFile()); + + if (log.isDebugEnabled() && file.exists()) { + log.debug("Overwriting file '{}'.", zipEntry.getName()); + } + + FileUtils.copy(in, file); + } + } + } + + private String getUnrootedName(String root, String name) { + return name.substring(root.length()); + } + + private String getRootName(final String name) { + String newName = name.substring(FilenameUtils.getPrefixLength(name)); + int idx = newName.indexOf(PATH_SEPARATOR); + if (idx < 0) { + throw new ZipException("Entry " + newName + " from the root of the zip is not supported"); + } + return newName.substring(0, newName.indexOf(PATH_SEPARATOR)); + } + } + + /** + * Unpacks a ZIP file to its own location. + *

+ * The ZIP file will be first renamed (using a temporary name). After the + * extraction it will be deleted. + * + * @param zip + * input ZIP file as well as the target directory. + * + * @see #unpack(File, File) + */ + public static void explode(File zip) { + try { + // Find a new unique name is the same directory + File tempFile = FileUtils.getTempFileFor(zip); + + // Rename the archive + FileUtils.moveFile(zip, tempFile); + + // Unpack it + unpack(tempFile, zip); + + // Delete the archive + if (!tempFile.delete()) { + throw new IOException("Unable to delete file: " + tempFile); + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /* Compressing single entries to ZIP files. */ + + /** + * Compresses the given file into a ZIP file with single entry. + * + * @param file file to be compressed. + * @return ZIP file created. + */ + public static byte[] packEntry(File file) { + log.trace("Compressing '{}' into a ZIP file with single entry.", file); + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + try { + ZipOutputStream out = new ZipOutputStream(result); + ZipEntry entry = ZipEntryUtil.fromFile(file.getName(), file); + InputStream in = new BufferedInputStream(new FileInputStream(file)); + try { + ZipEntryUtil.addEntry(entry, in, out); + } + finally { + IOUtils.closeQuietly(in); + } + out.close(); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + return result.toByteArray(); + } + + /* Compressing ZIP files. */ + + /** + * Compresses the given directory and all its sub-directories into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * Will not include the root directory name in the archive. + * + * @param rootDir + * root directory. + * @param zip + * ZIP file that will be created or overwritten. + */ + public static void pack(File rootDir, File zip) { + pack(rootDir, zip, DEFAULT_COMPRESSION_LEVEL); + } + + /** + * Compresses the given directory and all its sub-directories into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * Will not include the root directory name in the archive. + * + * @param rootDir + * root directory. + * @param zip + * ZIP file that will be created or overwritten. + * @param compressionLevel + * compression level + */ + public static void pack(File rootDir, File zip, int compressionLevel) { + pack(rootDir, zip, IdentityNameMapper.INSTANCE, compressionLevel); + } + + /** + * Compresses the given directory and all its sub-directories into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * Will not include the root directory name in the archive. + * + * @param sourceDir + * root directory. + * @param targetZipFile + * ZIP file that will be created or overwritten. + * @param preserveRoot + * true if the resulted archive should have the top directory entry + */ + public static void pack(final File sourceDir, final File targetZipFile, final boolean preserveRoot) { + if (preserveRoot) { + final String parentName = sourceDir.getName(); + pack(sourceDir, targetZipFile, new NameMapper() { + public String map(String name) { + return parentName + PATH_SEPARATOR + name; + } + }); + } + else { + pack(sourceDir, targetZipFile); + } + } + + /** + * Compresses the given file into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * + * @param fileToPack + * file that needs to be zipped. + * @param destZipFile + * ZIP file that will be created or overwritten. + */ + public static void packEntry(File fileToPack, File destZipFile) { + packEntry(fileToPack, destZipFile, IdentityNameMapper.INSTANCE); + } + + /** + * Compresses the given file into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * + * @param fileToPack + * file that needs to be zipped. + * @param destZipFile + * ZIP file that will be created or overwritten. + * @param fileName + * the name for the file inside the archive + */ + public static void packEntry(File fileToPack, File destZipFile, final String fileName) { + packEntry(fileToPack, destZipFile, new NameMapper() { + public String map(String name) { + return fileName; + } + }); + } + + /** + * Compresses the given file into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * + * @param fileToPack + * file that needs to be zipped. + * @param destZipFile + * ZIP file that will be created or overwritten. + * @param mapper + * call-back for renaming the entries. + */ + public static void packEntry(File fileToPack, File destZipFile, NameMapper mapper) { + packEntries(new File[] { fileToPack }, destZipFile, mapper); + } + + /** + * Compresses the given files into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * + * @param filesToPack + * files that needs to be zipped. + * @param destZipFile + * ZIP file that will be created or overwritten. + */ + public static void packEntries(File[] filesToPack, File destZipFile) { + packEntries(filesToPack, destZipFile, IdentityNameMapper.INSTANCE); + } + + /** + * Compresses the given files into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * + * @param filesToPack + * files that needs to be zipped. + * @param destZipFile + * ZIP file that will be created or overwritten. + * @param mapper + * call-back for renaming the entries. + */ + public static void packEntries(File[] filesToPack, File destZipFile, NameMapper mapper) { + log.debug("Compressing '{}' into '{}'.", filesToPack, destZipFile); + + ZipOutputStream out = null; + FileOutputStream fos = null; + try { + fos = new FileOutputStream(destZipFile); + out = new ZipOutputStream(new BufferedOutputStream(fos)); + + for (int i = 0; i < filesToPack.length; i++) { + File fileToPack = filesToPack[i]; + + ZipEntry zipEntry = ZipEntryUtil.fromFile(mapper.map(fileToPack.getName()), fileToPack); + out.putNextEntry(zipEntry); + FileUtils.copy(fileToPack, out); + out.closeEntry(); + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(fos); + } + } + + /** + * Compresses the given directory and all its sub-directories into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * + * @param sourceDir + * root directory. + * @param targetZip + * ZIP file that will be created or overwritten. + * @param mapper + * call-back for renaming the entries. + */ + public static void pack(File sourceDir, File targetZip, NameMapper mapper) { + pack(sourceDir, targetZip, mapper, DEFAULT_COMPRESSION_LEVEL); + } + + /** + * Compresses the given directory and all its sub-directories into a ZIP file. + *

+ * The ZIP file must not be a directory and its parent directory must exist. + * + * @param sourceDir + * root directory. + * @param targetZip + * ZIP file that will be created or overwritten. + * @param mapper + * call-back for renaming the entries. + * @param compressionLevel + * compression level + */ + public static void pack(File sourceDir, File targetZip, NameMapper mapper, int compressionLevel) { + log.debug("Compressing '{}' into '{}'.", sourceDir, targetZip); + if (!sourceDir.exists()) { + throw new ZipException("Given file '" + sourceDir + "' doesn't exist!"); + } + ZipOutputStream out = null; + try { + out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(targetZip))); + out.setLevel(compressionLevel); + pack(sourceDir, out, mapper, "", true); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Compresses the given directory and all its sub-directories into a ZIP file. + * + * @param dir + * root directory. + * @param out + * ZIP output stream. + * @param mapper + * call-back for renaming the entries. + * @param pathPrefix + * prefix to be used for the entries. + * @param mustHaveChildren + * if true, but directory to pack doesn't have any files, throw an exception. + */ + private static void pack(File dir, ZipOutputStream out, NameMapper mapper, String pathPrefix, boolean mustHaveChildren) throws IOException { + String[] filenames = dir.list(); + if (filenames == null) { + if (!dir.exists()) { + throw new ZipException("Given file '" + dir + "' doesn't exist!"); + } + throw new IOException("Given file is not a directory '" + dir + "'"); + } + + if (mustHaveChildren && filenames.length == 0) { + throw new ZipException("Given directory '" + dir + "' doesn't contain any files!"); + } + + for (int i = 0; i < filenames.length; i++) { + String filename = filenames[i]; + File file = new File(dir, filename); + boolean isDir = file.isDirectory(); + String path = pathPrefix + file.getName(); // NOSONAR + if (isDir) { + path += PATH_SEPARATOR; // NOSONAR + } + + // Create a ZIP entry + String name = mapper.map(path); + if (name != null) { + ZipEntry zipEntry = ZipEntryUtil.fromFile(name, file); + + out.putNextEntry(zipEntry); + + // Copy the file content + if (!isDir) { + FileUtils.copy(file, out); + } + + out.closeEntry(); + } + + // Traverse the directory + if (isDir) { + pack(file, out, mapper, path, false); + } + } + } + + /** + * Repacks a provided ZIP file into a new ZIP with a given compression level. + *

+ * + * @param srcZip + * source ZIP file. + * @param dstZip + * destination ZIP file. + * @param compressionLevel + * compression level. + */ + public static void repack(File srcZip, File dstZip, int compressionLevel) { + + log.debug("Repacking '{}' into '{}'.", srcZip, dstZip); + + RepackZipEntryCallback callback = new RepackZipEntryCallback(dstZip, compressionLevel); + + try { + iterate(srcZip, callback); + } + finally { + callback.closeStream(); + } + } + + /** + * Repacks a provided ZIP input stream into a ZIP file with a given compression level. + *

+ * + * @param is + * ZIP input stream. + * @param dstZip + * destination ZIP file. + * @param compressionLevel + * compression level. + */ + public static void repack(InputStream is, File dstZip, int compressionLevel) { + + log.debug("Repacking from input stream into '{}'.", dstZip); + + RepackZipEntryCallback callback = new RepackZipEntryCallback(dstZip, compressionLevel); + + try { + iterate(is, callback); + } + finally { + callback.closeStream(); + } + } + + /** + * Repacks a provided ZIP file and replaces old file with the new one. + *

+ * + * @param zip + * source ZIP file to be repacked and replaced. + * @param compressionLevel + * compression level. + */ + public static void repack(File zip, int compressionLevel) { + try { + File tmpZip = FileUtils.getTempFileFor(zip); + + repack(zip, tmpZip, compressionLevel); + + // Delete original zip + if (!zip.delete()) { + throw new IOException("Unable to delete the file: " + zip); + } + + // Rename the archive + FileUtils.moveFile(tmpZip, zip); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * RepackZipEntryCallback used in repacking methods. + * + * @author Pavel Grigorenko + */ + private static final class RepackZipEntryCallback implements ZipEntryCallback { + + private ZipOutputStream out; + + private RepackZipEntryCallback(File dstZip, int compressionLevel) { + try { + this.out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(dstZip))); + this.out.setLevel(compressionLevel); + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + ZipEntryUtil.copyEntry(zipEntry, in, out); + } + + private void closeStream() { + IOUtils.closeQuietly(out); + } + } + + /** + * Compresses a given directory in its own location. + *

+ * A ZIP file will be first created with a temporary name. After the + * compressing the directory will be deleted and the ZIP file will be renamed + * as the original directory. + * + * @param dir + * input directory as well as the target ZIP file. + * + * @see #pack(File, File) + */ + public static void unexplode(File dir) { + unexplode(dir, DEFAULT_COMPRESSION_LEVEL); + } + + /** + * Compresses a given directory in its own location. + *

+ * A ZIP file will be first created with a temporary name. After the + * compressing the directory will be deleted and the ZIP file will be renamed + * as the original directory. + * + * @param dir + * input directory as well as the target ZIP file. + * @param compressionLevel + * compression level + * + * @see #pack(File, File) + */ + public static void unexplode(File dir, int compressionLevel) { + try { + // Find a new unique name is the same directory + File zip = FileUtils.getTempFileFor(dir); + + // Pack it + pack(dir, zip, compressionLevel); + + // Delete the directory + FileUtils.deleteDirectory(dir); + + // Rename the archive + FileUtils.moveFile(zip, dir); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * Compresses the given entries into a new ZIP file. + * + * @param entries + * ZIP entries added. + * @param zip + * new ZIP file created. + */ + public static void pack(ZipEntrySource[] entries, File zip) { + log.debug("Creating '{}' from {}.", zip, Arrays.asList(entries)); + + ZipOutputStream out = null; + try { + out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zip))); + for (int i = 0; i < entries.length; i++) { + addEntry(entries[i], out); + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Copies an existing ZIP file and appends it with one new entry. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param file + * new entry to be added. + * @param destZip + * new ZIP file created. + */ + public static void addEntry(File zip, String path, File file, File destZip) { + addEntry(zip, new FileSource(path, file), destZip); + } + + /** + * Changes a zip file, adds one new entry in-place. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param file + * new entry to be added. + */ + public static void addEntry(final File zip, final String path, final File file) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + addEntry(zip, path, file, tmpFile); + return true; + } + }); + } + + /** + * Copies an existing ZIP file and appends it with one new entry. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param bytes + * new entry bytes (or null if directory). + * @param destZip + * new ZIP file created. + */ + public static void addEntry(File zip, String path, byte[] bytes, File destZip) { + addEntry(zip, new ByteSource(path, bytes), destZip); + } + + /** + * Copies an existing ZIP file and appends it with one new entry. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param bytes + * new entry bytes (or null if directory). + * @param destZip + * new ZIP file created. + * @param compressionLevel + * the new compression level (ZipEntry.STORED or ZipEntry.DEFLATED). + */ + public static void addEntry(File zip, String path, byte[] bytes, File destZip, final int compressionLevel) { + addEntry(zip, new ByteSource(path, bytes, compressionLevel), destZip); + } + + /** + * Changes a zip file, adds one new entry in-place. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param bytes + * new entry bytes (or null if directory). + */ + public static void addEntry(final File zip, final String path, final byte[] bytes) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + addEntry(zip, path, bytes, tmpFile); + return true; + } + }); + } + + /** + * Changes a zip file, adds one new entry in-place. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param bytes + * new entry bytes (or null if directory). + * @param compressionLevel + * the new compression level (ZipEntry.STORED or ZipEntry.DEFLATED). + */ + public static void addEntry(final File zip, final String path, final byte[] bytes, final int compressionLevel) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + addEntry(zip, path, bytes, tmpFile, compressionLevel); + return true; + } + }); + } + + + /** + * Copies an existing ZIP file and appends it with one new entry. + * + * @param zip + * an existing ZIP file (only read). + * @param entry + * new ZIP entry appended. + * @param destZip + * new ZIP file created. + */ + public static void addEntry(File zip, ZipEntrySource entry, File destZip) { + addEntries(zip, new ZipEntrySource[] { entry }, destZip); + } + + /** + * Changes a zip file, adds one new entry in-place. + * + * @param zip + * an existing ZIP file (only read). + * @param entry + * new ZIP entry appended. + */ + public static void addEntry(final File zip, final ZipEntrySource entry) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + addEntry(zip, entry, tmpFile); + return true; + } + }); + } + + /** + * Copies an existing ZIP file and appends it with new entries. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * new ZIP entries appended. + * @param destZip + * new ZIP file created. + */ + public static void addEntries(File zip, ZipEntrySource[] entries, File destZip) { + if (log.isDebugEnabled()) { + log.debug("Copying '" + zip + "' to '" + destZip + "' and adding " + Arrays.asList(entries) + "."); + } + + OutputStream destOut = null; + try { + destOut = new BufferedOutputStream(new FileOutputStream(destZip)); + addEntries(zip, entries, destOut); + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + finally { + IOUtils.closeQuietly(destOut); + } + } + + /** + * Copies an existing ZIP file and appends it with new entries. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * new ZIP entries appended. + * @param destOut + * new ZIP destination output stream + */ + public static void addEntries(File zip, ZipEntrySource[] entries, OutputStream destOut) { + if (log.isDebugEnabled()) { + log.debug("Copying '" + zip + "' to a stream and adding " + Arrays.asList(entries) + "."); + } + + ZipOutputStream out = null; + try { + out = new ZipOutputStream(destOut); + copyEntries(zip, out); + for (int i = 0; i < entries.length; i++) { + addEntry(entries[i], out); + } + out.finish(); + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + } + + /** + * Changes a zip file it with with new entries. in-place. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * new ZIP entries appended. + */ + public static void addEntries(final File zip, final ZipEntrySource[] entries) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + addEntries(zip, entries, tmpFile); + return true; + } + }); + } + + /** + * Copies an existing ZIP file and removes entry with a given path. + * + * @param zip + * an existing ZIP file (only read) + * @param path + * path of the entry to remove + * @param destZip + * new ZIP file created. + * @since 1.7 + */ + public static void removeEntry(File zip, String path, File destZip) { + removeEntries(zip, new String[] { path }, destZip); + } + + /** + * Changes an existing ZIP file: removes entry with a given path. + * + * @param zip + * an existing ZIP file + * @param path + * path of the entry to remove + * @since 1.7 + */ + public static void removeEntry(final File zip, final String path) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + removeEntry(zip, path, tmpFile); + return true; + } + }); + } + + /** + * Copies an existing ZIP file and removes entries with given paths. + * + * @param zip + * an existing ZIP file (only read) + * @param paths + * paths of the entries to remove + * @param destZip + * new ZIP file created. + * @since 1.7 + */ + public static void removeEntries(File zip, String[] paths, File destZip) { + if (log.isDebugEnabled()) { + log.debug("Copying '" + zip + "' to '" + destZip + "' and removing paths " + Arrays.asList(paths) + "."); + } + + ZipOutputStream out = null; + try { + out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); + copyEntries(zip, out, new HashSet(Arrays.asList(paths))); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Changes an existing ZIP file: removes entries with given paths. + * + * @param zip + * an existing ZIP file + * @param paths + * paths of the entries to remove + * @since 1.7 + */ + public static void removeEntries(final File zip, final String[] paths) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + removeEntries(zip, paths, tmpFile); + return true; + } + }); + } + + /** + * Copies all entries from one ZIP file to another. + * + * @param zip + * source ZIP file. + * @param out + * target ZIP stream. + */ + private static void copyEntries(File zip, final ZipOutputStream out) { + // this one doesn't call copyEntries with ignoredEntries, because that has poorer performance + final Set names = new HashSet(); + iterate(zip, new ZipEntryCallback() { + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + String entryName = zipEntry.getName(); + if (names.add(entryName)) { + ZipEntryUtil.copyEntry(zipEntry, in, out); + } + else if (log.isDebugEnabled()) { + log.debug("Duplicate entry: {}", entryName); + } + } + }); + } + + /** + * Copies all entries from one ZIP file to another, ignoring entries with path in ignoredEntries + * + * @param zip + * source ZIP file. + * @param out + * target ZIP stream. + * @param ignoredEntries + * paths of entries not to copy + */ + private static void copyEntries(File zip, final ZipOutputStream out, final Set ignoredEntries) { + final Set names = new HashSet(); + final Set dirNames = filterDirEntries(zip, ignoredEntries); + iterate(zip, new ZipEntryCallback() { + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + String entryName = zipEntry.getName(); + if (ignoredEntries.contains(entryName)) { + return; + } + + for(String dirName: dirNames) { + if (entryName.startsWith(dirName)) { + return; + } + } + + if (names.add(entryName)) { + ZipEntryUtil.copyEntry(zipEntry, in, out); + } + else if (log.isDebugEnabled()) { + log.debug("Duplicate entry: {}", entryName); + } + } + }); + } + + /** + * + * @param zip + * zip file to traverse + * @param names + * names of entries to filter dirs from + * @return Set names of entries that are dirs. + * + */ + static Set filterDirEntries(File zip, Collection names) { + Set dirs = new HashSet(); + if (zip == null) { + return dirs; + } + ZipFile zf = null; + try { + zf = new ZipFile(zip); + for(String entryName : names) { + ZipEntry entry = zf.getEntry(entryName); + if (entry != null) { + if (entry.isDirectory()) { + dirs.add(entry.getName()); + } + else if (zf.getInputStream(entry) == null) { + // no input stream means that this is a dir. + dirs.add(entry.getName() + PATH_SEPARATOR); + } + } + } + + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf); + } + return dirs; + } + + /** + * Copies an existing ZIP file and replaces a given entry in it. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param file + * new entry. + * @param destZip + * new ZIP file created. + * @return true if the entry was replaced. + */ + public static boolean replaceEntry(File zip, String path, File file, File destZip) { + return replaceEntry(zip, new FileSource(path, file), destZip); + } + + /** + * Changes an existing ZIP file: replaces a given entry in it. + * + * @param zip + * an existing ZIP file. + * @param path + * new ZIP entry path. + * @param file + * new entry. + * @return true if the entry was replaced. + */ + public static boolean replaceEntry(final File zip, final String path, final File file) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return replaceEntry(zip, new FileSource(path, file), tmpFile); + } + }); + } + + /** + * Copies an existing ZIP file and replaces a given entry in it. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param bytes + * new entry bytes (or null if directory). + * @param destZip + * new ZIP file created. + * @return true if the entry was replaced. + */ + public static boolean replaceEntry(File zip, String path, byte[] bytes, File destZip) { + return replaceEntry(zip, new ByteSource(path, bytes), destZip); + } + + /** + * Changes an existing ZIP file: replaces a given entry in it. + * + * @param zip + * an existing ZIP file. + * @param path + * new ZIP entry path. + * @param bytes + * new entry bytes (or null if directory). + * @return true if the entry was replaced. + */ + public static boolean replaceEntry(final File zip, final String path, final byte[] bytes) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return replaceEntry(zip, new ByteSource(path, bytes), tmpFile); + } + }); + } + + /** + * Changes an existing ZIP file: replaces a given entry in it. + * + * @param zip + * an existing ZIP file. + * @param path + * new ZIP entry path. + * @param bytes + * new entry bytes (or null if directory). + * @param compressionLevel + * the new compression level (ZipEntry.STORED or ZipEntry.DEFLATED). + * @return true if the entry was replaced. + */ + public static boolean replaceEntry(final File zip, final String path, final byte[] bytes, + final int compressionLevel) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return replaceEntry(zip, new ByteSource(path, bytes, compressionLevel), tmpFile); + } + }); + } + + /** + * Copies an existing ZIP file and replaces a given entry in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entry + * new ZIP entry. + * @param destZip + * new ZIP file created. + * @return true if the entry was replaced. + */ + public static boolean replaceEntry(File zip, ZipEntrySource entry, File destZip) { + return replaceEntries(zip, new ZipEntrySource[] { entry }, destZip); + } + + /** + * Changes an existing ZIP file: replaces a given entry in it. + * + * @param zip + * an existing ZIP file. + * @param entry + * new ZIP entry. + * @return true if the entry was replaced. + */ + public static boolean replaceEntry(final File zip, final ZipEntrySource entry) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return replaceEntry(zip, entry, tmpFile); + } + }); + } + + /** + * Copies an existing ZIP file and replaces the given entries in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * new ZIP entries to be replaced with. + * @param destZip + * new ZIP file created. + * @return true if at least one entry was replaced. + */ + public static boolean replaceEntries(File zip, ZipEntrySource[] entries, File destZip) { + if (log.isDebugEnabled()) { + log.debug("Copying '" + zip + "' to '" + destZip + "' and replacing entries " + Arrays.asList(entries) + "."); + } + + final Map entryByPath = entriesByPath(entries); + final int entryCount = entryByPath.size(); + try { + final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); + try { + final Set names = new HashSet(); + iterate(zip, new ZipEntryCallback() { + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + if (names.add(zipEntry.getName())) { + ZipEntrySource entry = (ZipEntrySource) entryByPath.remove(zipEntry.getName()); + if (entry != null) { + addEntry(entry, out); + } + else { + ZipEntryUtil.copyEntry(zipEntry, in, out); + } + } + else if (log.isDebugEnabled()) { + log.debug("Duplicate entry: {}", zipEntry.getName()); + } + } + }); + } + finally { + IOUtils.closeQuietly(out); + } + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + return entryByPath.size() < entryCount; + } + + /** + * Changes an existing ZIP file: replaces a given entry in it. + * + * @param zip + * an existing ZIP file. + * @param entries + * new ZIP entries to be replaced with. + * @return true if at least one entry was replaced. + */ + public static boolean replaceEntries(final File zip, final ZipEntrySource[] entries) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return replaceEntries(zip, entries, tmpFile); + } + }); + } + + /** + * Copies an existing ZIP file and adds/replaces the given entries in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * ZIP entries to be replaced or added. + * @param destZip + * new ZIP file created. + */ + public static void addOrReplaceEntries(File zip, ZipEntrySource[] entries, File destZip) { + if (log.isDebugEnabled()) { + log.debug("Copying '" + zip + "' to '" + destZip + "' and adding/replacing entries " + Arrays.asList(entries) + + "."); + } + + final Map entryByPath = entriesByPath(entries); + try { + final ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); + try { + // Copy and replace entries + final Set names = new HashSet(); + iterate(zip, new ZipEntryCallback() { + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + if (names.add(zipEntry.getName())) { + ZipEntrySource entry = (ZipEntrySource) entryByPath.remove(zipEntry.getName()); + if (entry != null) { + addEntry(entry, out); + } + else { + ZipEntryUtil.copyEntry(zipEntry, in, out); + } + } + else if (log.isDebugEnabled()) { + log.debug("Duplicate entry: {}", zipEntry.getName()); + } + } + }); + + // Add new entries + for (ZipEntrySource zipEntrySource : entryByPath.values()) { + addEntry(zipEntrySource, out); + } + } + finally { + IOUtils.closeQuietly(out); + } + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + } + + /** + * Changes a ZIP file: adds/replaces the given entries in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * ZIP entries to be replaced or added. + */ + public static void addOrReplaceEntries(final File zip, final ZipEntrySource[] entries) { + operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + addOrReplaceEntries(zip, entries, tmpFile); + return true; + } + }); + } + + /** + * @return given entries indexed by path. + */ + static Map entriesByPath(ZipEntrySource... entries) { + Map result = new HashMap(); + for (int i = 0; i < entries.length; i++) { + ZipEntrySource source = entries[i]; + result.put(source.getPath(), source); + } + return result; + } + + /** + * Copies an existing ZIP file and transforms a given entry in it. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param transformer + * transformer for the given ZIP entry. + * @param destZip + * new ZIP file created. + * @return true if the entry was replaced. + */ + public static boolean transformEntry(File zip, String path, ZipEntryTransformer transformer, File destZip) { + return transformEntry(zip, new ZipEntryTransformerEntry(path, transformer), destZip); + } + + /** + * Changes an existing ZIP file: transforms a given entry in it. + * + * @param zip + * an existing ZIP file (only read). + * @param path + * new ZIP entry path. + * @param transformer + * transformer for the given ZIP entry. + * @return true if the entry was replaced. + */ + public static boolean transformEntry(final File zip, final String path, final ZipEntryTransformer transformer) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return transformEntry(zip, path, transformer, tmpFile); + } + }); + } + + /** + * Copies an existing ZIP file and transforms a given entry in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entry + * transformer for a ZIP entry. + * @param destZip + * new ZIP file created. + * @return true if the entry was replaced. + */ + public static boolean transformEntry(File zip, ZipEntryTransformerEntry entry, File destZip) { + return transformEntries(zip, new ZipEntryTransformerEntry[] { entry }, destZip); + } + + /** + * Changes an existing ZIP file: transforms a given entry in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entry + * transformer for a ZIP entry. + * @return true if the entry was replaced. + */ + public static boolean transformEntry(final File zip, final ZipEntryTransformerEntry entry) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return transformEntry(zip, entry, tmpFile); + } + }); + } + + /** + * Copies an existing ZIP file and transforms the given entries in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * ZIP entry transformers. + * @param destZip + * new ZIP file created. + * @return true if at least one entry was replaced. + */ + public static boolean transformEntries(File zip, ZipEntryTransformerEntry[] entries, File destZip) { + if (log.isDebugEnabled()) + log.debug("Copying '" + zip + "' to '" + destZip + "' and transforming entries " + Arrays.asList(entries) + "."); + + try { + ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(destZip))); + try { + TransformerZipEntryCallback action = new TransformerZipEntryCallback(Arrays.asList(entries), out); + iterate(zip, action); + return action.found(); + } + finally { + IOUtils.closeQuietly(out); + } + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * Changes an existing ZIP file: transforms a given entries in it. + * + * @param zip + * an existing ZIP file (only read). + * @param entries + * ZIP entry transformers. + * @return true if the entry was replaced. + */ + public static boolean transformEntries(final File zip, final ZipEntryTransformerEntry[] entries) { + return operateInPlace(zip, new InPlaceAction() { + public boolean act(File tmpFile) { + return transformEntries(zip, entries, tmpFile); + } + }); + } + + /** + * Copies an existing ZIP file and transforms a given entry in it. + * + * @param is + * a ZIP input stream. + * @param path + * new ZIP entry path. + * @param transformer + * transformer for the given ZIP entry. + * @param os + * a ZIP output stream. + * @return true if the entry was replaced. + */ + public static boolean transformEntry(InputStream is, String path, ZipEntryTransformer transformer, OutputStream os) { + return transformEntry(is, new ZipEntryTransformerEntry(path, transformer), os); + } + + /** + * Copies an existing ZIP file and transforms a given entry in it. + * + * @param is + * a ZIP input stream. + * @param entry + * transformer for a ZIP entry. + * @param os + * a ZIP output stream. + * @return true if the entry was replaced. + */ + public static boolean transformEntry(InputStream is, ZipEntryTransformerEntry entry, OutputStream os) { + return transformEntries(is, new ZipEntryTransformerEntry[] { entry }, os); + } + + /** + * Copies an existing ZIP file and transforms the given entries in it. + * + * @param is + * a ZIP input stream. + * @param entries + * ZIP entry transformers. + * @param os + * a ZIP output stream. + * @return true if at least one entry was replaced. + */ + public static boolean transformEntries(InputStream is, ZipEntryTransformerEntry[] entries, OutputStream os) { + if (log.isDebugEnabled()) + log.debug("Copying '" + is + "' to '" + os + "' and transforming entries " + Arrays.asList(entries) + "."); + + try { + ZipOutputStream out = new ZipOutputStream(os); + TransformerZipEntryCallback action = new TransformerZipEntryCallback(Arrays.asList(entries), out); + iterate(is, action); + // Finishes writing the contents of the ZIP output stream without closing + // the underlying stream. + out.finish(); + return action.found(); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + private static class TransformerZipEntryCallback implements ZipEntryCallback { + + private final Map entryByPath; + private final int entryCount; + private final ZipOutputStream out; + private final Set names = new HashSet(); + + public TransformerZipEntryCallback(List entries, ZipOutputStream out) { + entryByPath = transformersByPath(entries); + entryCount = entryByPath.size(); + this.out = out; + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + if (names.add(zipEntry.getName())) { + ZipEntryTransformer entry = (ZipEntryTransformer) entryByPath.remove(zipEntry.getName()); + if (entry != null) { + entry.transform(in, zipEntry, out); + } + else { + ZipEntryUtil.copyEntry(zipEntry, in, out); + } + } + else if (log.isDebugEnabled()) { + log.debug("Duplicate entry: {}", zipEntry.getName()); + } + } + + /** + * @return true if at least one entry was replaced. + */ + public boolean found() { + return entryByPath.size() < entryCount; + } + + } + + /** + * @return transformers by path. + */ + static Map transformersByPath(List entries) { + Map result = new HashMap(); + for (ZipEntryTransformerEntry entry: entries) { + result.put(entry.getPath(), entry.getTransformer()); + } + return result; + } + + /** + * Adds a given ZIP entry to a ZIP file. + * + * @param entry + * new ZIP entry. + * @param out + * target ZIP stream. + */ + private static void addEntry(ZipEntrySource entry, ZipOutputStream out) throws IOException { + out.putNextEntry(entry.getEntry()); + InputStream in = entry.getInputStream(); + if (in != null) { + try { + IOUtils.copy(in, out); + } + finally { + IOUtils.closeQuietly(in); + } + } + out.closeEntry(); + } + + + + /* Comparing two ZIP files. */ + + /** + * Compares two ZIP files and returns true if they contain same + * entries. + *

+ * First the two files are compared byte-by-byte. If a difference is found the + * corresponding entries of both ZIP files are compared. Thus if same contents + * is packed differently the two archives may still be the same. + *

+ *

+ * Two archives are considered the same if + *

    + *
  1. they contain same number of entries,
  2. + *
  3. for each entry in the first archive there exists an entry with the same + * in the second archive
  4. + *
  5. for each entry in the first archive and the entry with the same name in + * the second archive + *
      + *
    1. both are either directories or files,
    2. + *
    3. both have the same size,
    4. + *
    5. both have the same CRC,
    6. + *
    7. both have the same contents (compared byte-by-byte).
    8. + *
    + *
  6. + *
+ * + * @param f1 + * first ZIP file. + * @param f2 + * second ZIP file. + * @return true if the two ZIP files contain same entries, + * false if a difference was found or an error occurred + * during the comparison. + */ + public static boolean archiveEquals(File f1, File f2) { + try { + // Check the files byte-by-byte + if (FileUtils.contentEquals(f1, f2)) { + return true; + } + + log.debug("Comparing archives '{}' and '{}'...", f1, f2); + + long start = System.currentTimeMillis(); + boolean result = archiveEqualsInternal(f1, f2); + long time = System.currentTimeMillis() - start; + if (time > 0) { + log.debug("Archives compared in " + time + " ms."); + } + return result; + } + catch (Exception e) { + log.debug("Could not compare '" + f1 + "' and '" + f2 + "':", e); + return false; + } + } + + private static boolean archiveEqualsInternal(File f1, File f2) throws IOException { + ZipFile zf1 = null; + ZipFile zf2 = null; + try { + zf1 = new ZipFile(f1); + zf2 = new ZipFile(f2); + + // Check the number of entries + if (zf1.size() != zf2.size()) { + log.debug("Number of entries changed (" + zf1.size() + " vs " + zf2.size() + ")."); + return false; + } + /* + * As there are same number of entries in both archives we can traverse + * all entries of one of the archives and get the corresponding entries + * from the other archive. + * + * If a corresponding entry is missing from the second archive the + * archives are different and we finish the comparison. + * + * We guarantee that no entry of the second archive is skipped as there + * are same number of unique entries in both archives. + */ + Enumeration en = zf1.entries(); + while (en.hasMoreElements()) { + ZipEntry e1 = (ZipEntry) en.nextElement(); + String path = e1.getName(); + ZipEntry e2 = zf2.getEntry(path); + + // Check meta data + if (!metaDataEquals(path, e1, e2)) { + return false; + } + + // Check the content + InputStream is1 = null; + InputStream is2 = null; + try { + is1 = zf1.getInputStream(e1); + is2 = zf2.getInputStream(e2); + + if (!IOUtils.contentEquals(is1, is2)) { + log.debug("Entry '{}' content changed.", path); + return false; + } + } + finally { + IOUtils.closeQuietly(is1); + IOUtils.closeQuietly(is2); + } + } + } + finally { + closeQuietly(zf1); + closeQuietly(zf2); + } + + log.debug("Archives are the same."); + + return true; + } + + /** + * Compares meta-data of two ZIP entries. + *

+ * Two entries are considered the same if + *

    + *
  1. both entries exist,
  2. + *
  3. both entries are either directories or files,
  4. + *
  5. both entries have the same size,
  6. + *
  7. both entries have the same CRC.
  8. + *
+ * + * @param path + * name of the entries. + * @param e1 + * first entry (required). + * @param e2 + * second entry (may be null). + * @return true if no difference was found. + */ + private static boolean metaDataEquals(String path, ZipEntry e1, ZipEntry e2) throws IOException { + // Check if the same entry exists in the second archive + if (e2 == null) { + log.debug("Entry '{}' removed.", path); + return false; + } + + // Check the directory flag + if (e1.isDirectory()) { + if (e2.isDirectory()) { + return true; // Let's skip the directory as there is nothing to compare + } + else { + log.debug("Entry '{}' not a directory any more.", path); + return false; + } + } + else if (e2.isDirectory()) { + log.debug("Entry '{}' now a directory.", path); + return false; + } + + // Check the size + long size1 = e1.getSize(); + long size2 = e2.getSize(); + if (size1 != -1 && size2 != -1 && size1 != size2) { + log.debug("Entry '" + path + "' size changed (" + size1 + " vs " + size2 + ")."); + return false; + } + + // Check the CRC + long crc1 = e1.getCrc(); + long crc2 = e2.getCrc(); + if (crc1 != -1 && crc2 != -1 && crc1 != crc2) { + log.debug("Entry '" + path + "' CRC changed (" + crc1 + " vs " + crc2 + ")."); + return false; + } + + // Check the time (ignored, logging only) + if (log.isTraceEnabled()) { + long time1 = e1.getTime(); + long time2 = e2.getTime(); + if (time1 != -1 && time2 != -1 && time1 != time2) { + log.trace("Entry '" + path + "' time changed (" + new Date(time1) + " vs " + new Date(time2) + ")."); + } + } + + return true; + } + + /** + * Compares same entry in two ZIP files (byte-by-byte). + * + * @param f1 + * first ZIP file. + * @param f2 + * second ZIP file. + * @param path + * name of the entry. + * @return true if the contents of the entry was same in both ZIP + * files. + */ + public static boolean entryEquals(File f1, File f2, String path) { + return entryEquals(f1, f2, path, path); + } + + /** + * Compares two ZIP entries (byte-by-byte). . + * + * @param f1 + * first ZIP file. + * @param f2 + * second ZIP file. + * @param path1 + * name of the first entry. + * @param path2 + * name of the second entry. + * @return true if the contents of the entries were same. + */ + public static boolean entryEquals(File f1, File f2, String path1, String path2) { + ZipFile zf1 = null; + ZipFile zf2 = null; + + try { + zf1 = new ZipFile(f1); + zf2 = new ZipFile(f2); + + return doEntryEquals(zf1, zf2, path1, path2); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + closeQuietly(zf1); + closeQuietly(zf2); + } + } + + /** + * Compares two ZIP entries (byte-by-byte). . + * + * @param zf1 + * first ZIP file. + * @param zf2 + * second ZIP file. + * @param path1 + * name of the first entry. + * @param path2 + * name of the second entry. + * @return true if the contents of the entries were same. + */ + public static boolean entryEquals(ZipFile zf1, ZipFile zf2, String path1, String path2) { + try { + return doEntryEquals(zf1, zf2, path1, path2); + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + } + + /** + * Compares two ZIP entries (byte-by-byte). . + * + * @param zf1 + * first ZIP file. + * @param zf2 + * second ZIP file. + * @param path1 + * name of the first entry. + * @param path2 + * name of the second entry. + * @return true if the contents of the entries were same. + */ + private static boolean doEntryEquals(ZipFile zf1, ZipFile zf2, String path1, String path2) throws IOException { + InputStream is1 = null; + InputStream is2 = null; + try { + ZipEntry e1 = zf1.getEntry(path1); + ZipEntry e2 = zf2.getEntry(path2); + + if (e1 == null && e2 == null) { + return true; + } + + if (e1 == null || e2 == null) { + return false; + } + + is1 = zf1.getInputStream(e1); + is2 = zf2.getInputStream(e2); + if (is1 == null && is2 == null) { + return true; + } + if (is1 == null || is2 == null) { + return false; + } + + return IOUtils.contentEquals(is1, is2); + } + finally { + IOUtils.closeQuietly(is1); + IOUtils.closeQuietly(is2); + } + } + + /** + * Closes the ZIP file while ignoring any errors. + * + * @param zf + * ZIP file to be closed. + */ + public static void closeQuietly(ZipFile zf) { + try { + if (zf != null) { + zf.close(); + } + } + catch (IOException e) { + } + } + + /** + * Simple helper to make inplace operation easier + * + * @author shelajev + */ + private abstract static class InPlaceAction { + + /** + * @return true if something has been changed during the action. + */ + abstract boolean act(File tmpFile); + } + + /** + * + * This method provides a general infrastructure for in-place operations. + * It creates temp file as a destination, then invokes the action on source and destination. + * Then it copies the result back into src file. + * + * @param src - source zip file we want to modify + * @param action - action which actually modifies the archives + * + * @return result of the action + */ + private static boolean operateInPlace(File src, InPlaceAction action) { + File tmp = null; + try { + tmp = File.createTempFile("zt-zip-tmp", ".zip"); + boolean result = action.act(tmp); + if (result) { // else nothing changes + FileUtils.forceDelete(src); + FileUtils.moveFile(tmp, src); + } + return result; + } + catch (IOException e) { + throw ZipExceptionUtil.rethrow(e); + } + finally { + FileUtils.deleteQuietly(tmp); + } + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/Zips.java b/app/src/main/java/org/zeroturnaround/zip/Zips.java new file mode 100644 index 0000000..fc577ae --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/Zips.java @@ -0,0 +1,742 @@ +/** + * Copyright (C) 2012 ZeroTurnaround LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zeroturnaround.zip; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.zeroturnaround.zip.commons.FileUtils; +import org.zeroturnaround.zip.commons.IOUtils; +import org.zeroturnaround.zip.transform.ZipEntryTransformer; +import org.zeroturnaround.zip.transform.ZipEntryTransformerEntry; + +/** + * Fluent api for zip handling. + * + * @author shelajev + * + */ +public class Zips { + + /** + * Source archive. + */ + private final File src; + + /** + * Optional destination archive, if null, src will be overwritten + */ + private File dest; + + /** + * Charset to use for entry names + */ + private Charset charset; + + /** + * Flag to carry timestamps of entries on. + */ + private boolean preserveTimestamps; + + /** + * List + */ + private List changedEntries = new ArrayList(); + + /** + * Set + */ + private Set removedEntries = new HashSet(); + + /** + * List + */ + private List transformers = new ArrayList(); + + /* + * If you want many name mappers here, you can create some compound instance that knows if + * it wants to stop after first successfull mapping or go through all transformations, if null means + * stop and ignore entry or that name mapper didn't know how to transform, etc. + */ + private NameMapper nameMapper; + + /** + * Flag to show that we want the final result to be unpacked + */ + private boolean unpackedResult; + + private Zips(File src) { + this.src = src; + } + + /** + * Static factory method to obtain an instance of Zips. + * + * @param src zip file to process + * @return instance of Zips + */ + public static Zips get(File src) { + return new Zips(src); + } + + /** + * Static factory method to obtain an instance of Zips without source file. + * See {@link #get(File src)}. + * + * @return instance of Zips + */ + public static Zips create() { + return new Zips(null); + } + + /** + * Specifies an entry to add or change to the output when this Zips executes. + * Adding takes precedence over removal of entries. + * + * @param entry entry to add + * @return this Zips for fluent api + */ + public Zips addEntry(ZipEntrySource entry) { + this.changedEntries.add(entry); + return this; + } + + /** + * Specifies entries to add or change to the output when this Zips executes. + * Adding takes precedence over removal of entries. + * + * @param entries entries to add + * @return this Zips for fluent api + */ + public Zips addEntries(ZipEntrySource[] entries) { + this.changedEntries.addAll(Arrays.asList(entries)); + return this; + } + + /** + * Adds a file entry. If given file is a dir, adds it and all subfiles recursively. + * Adding takes precedence over removal of entries. + * + * @param file file to add. + * @return this Zips for fluent api + */ + public Zips addFile(File file) { + return addFile(file, false, null); + } + + /** + * Adds a file entry. If given file is a dir, adds it and all subfiles recursively. + * Adding takes precedence over removal of entries. + * + * @param file file to add. + * @param preserveRoot if file is a directory, true indicates we want to preserve this dir in the zip. + * otherwise children of the file are added directly under root. + * @return this Zips for fluent api + */ + public Zips addFile(File file, boolean preserveRoot) { + return addFile(file, preserveRoot, null); + } + + /** + * Adds a file entry. If given file is a dir, adds it and all subfiles recursively. + * Adding takes precedence over removal of entries. + * + * @param file file to add. + * @param filter a filter to accept files for adding, null means all files are accepted + * @return this Zips for fluent api + */ + public Zips addFile(File file, FileFilter filter) { + return this.addFile(file, false, filter); + } + + /** + * Adds a file entry. If given file is a dir, adds it and all subfiles recursively. + * Adding takes precedence over removal of entries. + * + * @param file file to add. + * @param preserveRoot if file is a directory, true indicates we want to preserve this dir in the zip. + * otherwise children of the file are added directly under root. + * @param filter a filter to accept files for adding, null means all files are accepted + * @return this Zips for fluent api + */ + public Zips addFile(File file, boolean preserveRoot, FileFilter filter) { + if (!file.isDirectory()) { + this.changedEntries.add(new FileSource(file.getName(), file)); + return this; + } + + Collection files = ZTFileUtil.listFiles(file); + for (File entryFile : files) { + if (filter != null && !filter.accept(entryFile)) { + continue; + } + String entryPath = getRelativePath(file, entryFile); + if (File.separator.equals("\\")) { + // replace directory separators on windows as at least 7zip packs zip with entries having "/" like on linux + entryPath = entryPath.replace('\\', '/'); + } + if (preserveRoot) { + entryPath = file.getName() + entryPath; + } + if (entryPath.startsWith("/")) { + entryPath = entryPath.substring(1); + } + this.changedEntries.add(new FileSource(entryPath, entryFile)); + } + return this; + } + + private String getRelativePath(File parent, File file) { + String parentPath = parent.getPath(); + String filePath = file.getPath(); + if (!filePath.startsWith(parentPath)) { + throw new IllegalArgumentException("File " + file + " is not a child of " + parent); + } + return filePath.substring(parentPath.length()); + } + + /** + * Specifies an entry to remove to the output when this Zips executes. + * + * @param entry path of the entry to remove + * @return this Zips for fluent api + */ + public Zips removeEntry(String entry) { + this.removedEntries.add(entry); + return this; + } + + /** + * Specifies entries to remove to the output when this Zips executes. + * + * @param entries paths of the entry to remove + * @return this Zips for fluent api + */ + public Zips removeEntries(String[] entries) { + this.removedEntries.addAll(Arrays.asList(entries)); + return this; + } + + /** + * Enables timestamp preserving for this Zips execution + * + * @return this Zips for fluent api + */ + public Zips preserveTimestamps() { + this.preserveTimestamps = true; + return this; + } + + /** + * Specifies timestamp preserving for this Zips execution + * + * @param preserve flag to preserve timestamps + * @return this Zips for fluent api + */ + public Zips setPreserveTimestamps(boolean preserve) { + this.preserveTimestamps = preserve; + return this; + } + + /** + * Specifies charset for this Zips execution + * + * @param charset charset to use + * @return this Zips for fluent api + */ + public Zips charset(Charset charset) { + this.charset = charset; + return this; + } + + /** + * Specifies destination file for this Zips execution, + * if destination is null (default value), then source file will be overwritten. + * Temporary file will be used as destination and then written over the source to + * create an illusion if inplace action. + * + * @param destination charset to use + * @return this Zips for fluent api + */ + public Zips destination(File destination) { + this.dest = destination; + return this; + } + + /** + * + * @param nameMapper to use when processing entries + * @return this Zips for fluent api + */ + public Zips nameMapper(NameMapper nameMapper) { + this.nameMapper = nameMapper; + return this; + } + + public Zips unpack() { + this.unpackedResult = true; + return this; + } + + /** + * @return true if destination is not specified. + */ + private boolean isInPlace() { + return dest == null; + } + + /** + * @return should the result of the processing be unpacked. + */ + private boolean isUnpack() { + return unpackedResult || (dest != null && dest.isDirectory()); + } + + /** + * Registers a transformer for a given entry. + * + * @param path entry to transform + * @param transformer transformer for the entry + * @return this Zips for fluent api + */ + public Zips addTransformer(String path, ZipEntryTransformer transformer) { + this.transformers.add(new ZipEntryTransformerEntry(path, transformer)); + return this; + } + + /** + * Iterates through source Zip entries removing or changing them according to + * set parameters. + */ + public void process() { + if (src == null && dest == null) { + throw new IllegalArgumentException("Source and destination shouldn't be null together"); + } + + File destinationFile = null; + try { + destinationFile = getDestinationFile(); + ZipOutputStream out = null; + ZipEntryOrInfoAdapter zipEntryAdapter = null; + + if (destinationFile.isFile()) { + out = ZipFileUtil.createZipOutputStream(new BufferedOutputStream(new FileOutputStream(destinationFile)), charset); + zipEntryAdapter = new ZipEntryOrInfoAdapter(new CopyingCallback(transformers, out, preserveTimestamps), null); + } + else { // directory + zipEntryAdapter = new ZipEntryOrInfoAdapter(new UnpackingCallback(transformers, destinationFile), null); + } + try { + processAllEntries(zipEntryAdapter); + } + finally { + IOUtils.closeQuietly(out); + } + handleInPlaceActions(destinationFile); + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + finally { + if (isInPlace()) { + // destinationZip is a temporary file + FileUtils.deleteQuietly(destinationFile); + } + } + } + + private void processAllEntries(ZipEntryOrInfoAdapter zipEntryAdapter) { + iterateChangedAndAdded(zipEntryAdapter); + iterateExistingExceptRemoved(zipEntryAdapter); + } + + private File getDestinationFile() throws IOException { + if(isUnpack()) { + if(isInPlace()) { + File tempFile = File.createTempFile("zips", null); + FileUtils.deleteQuietly(tempFile); + tempFile.mkdirs(); // temp dir created + return tempFile; + } + else { + if (!dest.isDirectory()) { + // destination is a directory, actually we shouldn't be here, because this should mean we want an unpacked result. + FileUtils.deleteQuietly(dest); + File result = new File(dest.getAbsolutePath()); + result.mkdirs(); // create a directory instead of dest file + return result; + } + return dest; + } + } + else { + // we need a file + if(isInPlace()) { // no destination specified, temp file + return File.createTempFile("zips", ".zip"); + } + else { + if(dest.isDirectory()) { + // destination is a directory, actually we shouldn't be here, because this should mean we want an unpacked result. + FileUtils.deleteQuietly(dest); + return new File(dest.getAbsolutePath()); + } + return dest; + } + } + } + + /** + * Reads the source ZIP file and executes the given callback for each entry. + *

+ * For each entry the corresponding input stream is also passed to the callback. If you want to stop the loop then throw a ZipBreakException. + * + * This method is charset aware and uses Zips.charset. + * + * @param zipEntryCallback + * callback to be called for each entry. + * + * @see ZipEntryCallback + * + */ + public void iterate(ZipEntryCallback zipEntryCallback) { + ZipEntryOrInfoAdapter zipEntryAdapter = new ZipEntryOrInfoAdapter(zipEntryCallback, null); + processAllEntries(zipEntryAdapter); + } + + /** + * Scans the source ZIP file and executes the given callback for each entry. + *

+ * Only the meta-data without the actual data is read. If you want to stop the loop then throw a ZipBreakException. + * + * This method is charset aware and uses Zips.charset. + * + * @param callback + * callback to be called for each entry. + * + * @see ZipInfoCallback + * @see #iterate(ZipEntryCallback) + */ + public void iterate(ZipInfoCallback callback) { + ZipEntryOrInfoAdapter zipEntryAdapter = new ZipEntryOrInfoAdapter(null, callback); + + processAllEntries(zipEntryAdapter); + } + + /** + * Alias to ZipUtil.getEntry() + * + * @param name + * name of the entry to fetch bytes from + * @return byte[] + * contents of the entry by given name + */ + public byte[] getEntry(String name) { + if (src == null) { + throw new IllegalStateException("Source is not given"); + } + return ZipUtil.unpackEntry(src, name); + } + + /** + * Alias to ZipUtil.containsEntry() + * + * @param name + * entry to check existence of + * @return true if zip archive we're processing contains entry by given name, false otherwise + */ + public boolean containsEntry(String name) { + if (src == null) { + throw new IllegalStateException("Source is not given"); + } + return ZipUtil.containsEntry(src, name); + } + + // ///////////// private api /////////////// + + /** + * Iterate through source for not removed entries with a given callback + * + * @param zipEntryCallback callback to execute on entries or their info. + */ + private void iterateExistingExceptRemoved(ZipEntryOrInfoAdapter zipEntryCallback) { + if (src == null) { + // if we don't have source specified, then we have nothing to iterate. + return; + } + final Set removedDirs = ZipUtil.filterDirEntries(src, removedEntries); + + ZipFile zf = null; + try { + zf = getZipFile(); + + // manage existing entries + Enumeration en = zf.entries(); + while (en.hasMoreElements()) { + ZipEntry entry = en.nextElement(); + String entryName = entry.getName(); + if (removedEntries.contains(entryName) || isEntryInDir(removedDirs, entryName)) { + // removed entries are + continue; + } + + if (nameMapper != null) { + String mappedName = nameMapper.map(entry.getName()); + if (mappedName == null) { + continue; // we should ignore this entry + } + else if (!mappedName.equals(entry.getName())) { + // if name is different, do nothing + entry = ZipEntryUtil.copy(entry, mappedName); + } + } + + InputStream is = zf.getInputStream(entry); + try { + zipEntryCallback.process(is, entry); + } + catch (ZipBreakException ex) { + break; + } + finally { + IOUtils.closeQuietly(is); + } + } + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + finally { + ZipUtil.closeQuietly(zf); + } + } + + /** + * Iterate through ZipEntrySources for added or changed entries with a given callback + * + * @param zipEntryCallback callback to execute on entries or their info + */ + private void iterateChangedAndAdded(ZipEntryOrInfoAdapter zipEntryCallback) { + + for (ZipEntrySource entrySource : changedEntries) { + InputStream entrySourceStream = null; + try { + ZipEntry entry = entrySource.getEntry(); + if (nameMapper != null) { + String mappedName = nameMapper.map(entry.getName()); + if (mappedName == null) { + continue; // we should ignore this entry + } + else if (!mappedName.equals(entry.getName())) { + // if name is different, do nothing + entry = ZipEntryUtil.copy(entry, mappedName); + } + } + entrySourceStream = entrySource.getInputStream(); + zipEntryCallback.process(entrySourceStream, entry); + } + catch (ZipBreakException ex) { + break; + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + finally { + IOUtils.closeQuietly(entrySourceStream); + } + } + } + + /** + * if we are doing something in place, move result file into src. + * + * @param result destination zip file + */ + private void handleInPlaceActions(File result) throws IOException { + if (isInPlace()) { + // we operate in-place + FileUtils.forceDelete(src); + if (result.isFile()) { + FileUtils.moveFile(result, src); + } + else { + FileUtils.moveDirectory(result, src); + } + } + } + + /** + * Checks if entry given by name resides inside of one of the dirs. + * + * @param dirNames dirs + * @param entryName entryPath + */ + private boolean isEntryInDir(Set dirNames, String entryName) { + // this should be done with a trie, put dirNames in a trie and check if entryName leads to + // some node or not. + for(String dirName : dirNames) { + if (entryName.startsWith(dirName)) { + return true; + } + } + return false; + } + + /** + * Creates a ZipFile from src and charset of this object. If a constructor with charset is + * not available, throws an exception. + * + * @return ZipFile + * @throws IOException if ZipFile cannot be constructed + * @throws IllegalArgumentException if accessing constructor ZipFile(File, Charset) + * + */ + private ZipFile getZipFile() throws IOException { + return ZipFileUtil.getZipFile(src, charset); + } + + private static class CopyingCallback implements ZipEntryCallback { + + private final Map entryByPath; + private final ZipOutputStream out; + private final Set visitedNames; + private final boolean preserveTimestapms; + + private CopyingCallback(List transformerEntries, ZipOutputStream out, boolean preserveTimestapms) { + this.out = out; + this.preserveTimestapms = preserveTimestapms; + entryByPath = ZipUtil.transformersByPath(transformerEntries); + visitedNames = new HashSet(); + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + String entryName = zipEntry.getName(); + + if (visitedNames.contains(entryName)) { + return; + } + visitedNames.add(entryName); + + ZipEntryTransformer transformer = (ZipEntryTransformer) entryByPath.remove(entryName); + if (transformer == null) { // no transformer + ZipEntryUtil.copyEntry(zipEntry, in, out, preserveTimestapms); + } + else { // still transfom entry + transformer.transform(in, zipEntry, out); + } + } + } + + private static class UnpackingCallback implements ZipEntryCallback { + + private final Map entryByPath; + private final Set visitedNames; + private final File destination; + + private UnpackingCallback(List entries, File destination) { + this.destination = destination; + this.entryByPath = ZipUtil.transformersByPath(entries); + visitedNames = new HashSet(); + } + + public void process(InputStream in, ZipEntry zipEntry) throws IOException { + String entryName = zipEntry.getName(); + + if (visitedNames.contains(entryName)) { + return; + } + visitedNames.add(entryName); + + File file = new File(destination, entryName); + if (zipEntry.isDirectory()) { + FileUtils.forceMkdir(file); + return; + } + else { + FileUtils.forceMkdir(file.getParentFile()); + file.createNewFile(); + } + + ZipEntryTransformer transformer = (ZipEntryTransformer) entryByPath.remove(entryName); + if (transformer == null) { // no transformer + FileUtils.copy(in, file); + } + else { // still transform entry + transformIntoFile(transformer, in, zipEntry, file); + } + } + + private void transformIntoFile(final ZipEntryTransformer transformer, final InputStream entryIn, final ZipEntry zipEntry, final File destination) throws IOException { + final PipedInputStream pipedIn = new PipedInputStream(); + final PipedOutputStream pipedOut = new PipedOutputStream(pipedIn); + + final ZipOutputStream zipOut = new ZipOutputStream(pipedOut); + final ZipInputStream zipIn = new ZipInputStream(pipedIn); + + ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(1); + + try { + newFixedThreadPool.execute(new Runnable() { + public void run() { + try { + transformer.transform(entryIn, zipEntry, zipOut); + } + catch (IOException e) { + ZipExceptionUtil.rethrow(e); + } + } + }); + zipIn.getNextEntry(); + FileUtils.copy(zipIn, destination); + } + finally { + try { + zipIn.closeEntry(); + } + catch (IOException e) { + // closing quietly + } + + newFixedThreadPool.shutdown(); + IOUtils.closeQuietly(pipedIn); + IOUtils.closeQuietly(zipIn); + IOUtils.closeQuietly(pipedOut); + IOUtils.closeQuietly(zipOut); + } + + } + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/commons/FileUtils.java b/app/src/main/java/org/zeroturnaround/zip/commons/FileUtils.java new file mode 100644 index 0000000..c3816e0 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/commons/FileUtils.java @@ -0,0 +1,966 @@ +/* + * 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.zeroturnaround.zip.commons; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache commons-io package. All license and other documentation is intact. + * + * General file manipulation utilities. + *

+ * Facilities are provided in the following areas: + *

    + *
  • writing to a file + *
  • reading from a file + *
  • make a directory including parent directories + *
  • copying files and directories + *
  • deleting files and directories + *
  • converting to and from a URL + *
  • listing files and directories by filter and extension + *
  • comparing file content + *
  • file last changed date + *
  • calculating a checksum + *
+ *

+ * Origin of code: Excalibur, Alexandria, Commons-Utils + * + * @author Kevin A. Burton + * @author Scott Sanders + * @author Daniel Rall + * @author Christoph.Reck + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Jeremias Maerki + * @author Stephen Colebourne + * @author Ian Springer + * @author Chris Eldredge + * @author Jim Harrington + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id: FileUtils.java 610810 2008-01-10 15:04:49Z niallp $ + */ +public class FileUtils { + + /** + * Instances should NOT be constructed in standard programming. + */ + public FileUtils() { + super(); + } + + /** + * The number of bytes in a kilobyte. + */ + public static final long ONE_KB = 1024; + + /** + * The number of bytes in a megabyte. + */ + public static final long ONE_MB = ONE_KB * ONE_KB; + + /** + * The number of bytes in a gigabyte. + */ + public static final long ONE_GB = ONE_KB * ONE_MB; + + /** + * An empty array of type File. + */ + public static final File[] EMPTY_FILE_ARRAY = new File[0]; + + /** + * Copies the given file into an output stream. + * + * @param file input file (must exist). + * @param out output stream. + * + * @throws java.io.IOException if file is not found or copying fails + */ + public static void copy(File file, OutputStream out) throws IOException { + FileInputStream in = new FileInputStream(file); + try { + IOUtils.copy(new BufferedInputStream(in), out); + } + finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Copies the given input stream into a file. + *

+ * The target file must not be a directory and its parent must exist. + * + * @param in source stream. + * @param file output file to be created or overwritten. + * + * @throws java.io.IOException if file is not found or copying fails + */ + public static void copy(InputStream in, File file) throws IOException { + OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); + try { + IOUtils.copy(in, out); + } + finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Find a non-existing file in the same directory using the same name as prefix. + * + * @param file file used for the name and location (it is not read or written). + * @return a non-existing file in the same directory using the same name as prefix. + */ + public static File getTempFileFor(File file) { + File parent = file.getParentFile(); + String name = file.getName(); + File result; + int index = 0; + do { + result = new File(parent, name + "_" + index++); + } + while (result.exists()); + return result; + } + + // ----------------------------------------------------------------------- + /** + * Opens a {@link FileInputStream} for the specified file, providing better + * error messages than simply calling new FileInputStream(file). + *

+ * At the end of the method either the stream will be successfully opened, or an exception will have been thrown. + *

+ * An exception is thrown if the file does not exist. An exception is thrown if the file object exists but is a directory. An exception is thrown if the file exists but cannot be + * read. + * + * @param file the file to open for input, must not be null + * @return a new {@link FileInputStream} for the specified file + * @throws FileNotFoundException if the file does not exist + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be read + * @since Commons IO 1.3 + */ + public static FileInputStream openInputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (file.canRead() == false) { + throw new IOException("File '" + file + "' cannot be read"); + } + } + else { + throw new FileNotFoundException("File '" + file + "' does not exist"); + } + return new FileInputStream(file); + } + + // ----------------------------------------------------------------------- + /** + * Opens a {@link FileOutputStream} for the specified file, checking and + * creating the parent directory if it does not exist. + *

+ * At the end of the method either the stream will be successfully opened, or an exception will have been thrown. + *

+ * The parent directory will be created if it does not exist. The file will be created if it does not exist. An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be written to. An exception is thrown if the parent directory cannot be created. + * + * @param file the file to open for output, must not be null + * @return a new {@link FileOutputStream} for the specified file + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be written to + * @throws IOException if a parent directory needs creating but that fails + * @since Commons IO 1.3 + */ + public static FileOutputStream openOutputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (file.canWrite() == false) { + throw new IOException("File '" + file + "' cannot be written to"); + } + } + else { + File parent = file.getParentFile(); + if (parent != null && parent.exists() == false) { + if (parent.mkdirs() == false) { + throw new IOException("File '" + file + "' could not be created"); + } + } + } + return new FileOutputStream(file); + } + + // ----------------------------------------------------------------------- + /** + * Compares the contents of two files to determine if they are equal or not. + *

+ * This method checks to see if the two files are different lengths or if they point to the same file, before resorting to byte-by-byte comparison of the contents. + *

+ * Code origin: Avalon + * + * @param file1 the first file + * @param file2 the second file + * @return true if the content of the files are equal or they both don't + * exist, false otherwise + * @throws IOException in case of an I/O error + */ + public static boolean contentEquals(File file1, File file2) throws IOException { + boolean file1Exists = file1.exists(); + if (file1Exists != file2.exists()) { + return false; + } + + if (!file1Exists) { + // two not existing files are equal + return true; + } + + if (file1.isDirectory() || file2.isDirectory()) { + // don't want to compare directory contents + throw new IOException("Can't compare directories, only files"); + } + + if (file1.length() != file2.length()) { + // lengths differ, cannot be equal + return false; + } + + if (file1.getCanonicalFile().equals(file2.getCanonicalFile())) { + // same file + return true; + } + + InputStream input1 = null; + InputStream input2 = null; + try { + input1 = new FileInputStream(file1); + input2 = new FileInputStream(file2); + return IOUtils.contentEquals(input1, input2); + + } + finally { + IOUtils.closeQuietly(input1); + IOUtils.closeQuietly(input2); + } + } + + // ----------------------------------------------------------------------- + /** + * Copies a file to a directory preserving the file date. + *

+ * This method copies the contents of the specified source file to a file of the same name in the specified destination directory. The destination directory is created if it does + * not exist. If the destination file exists, then this method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destDir the directory to place the copy in, must not be null + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFile(File, File, boolean) + */ + public static void copyFileToDirectory(File srcFile, File destDir) throws IOException { + copyFileToDirectory(srcFile, destDir, true); + } + + /** + * Copies a file to a directory optionally preserving the file date. + *

+ * This method copies the contents of the specified source file to a file of the same name in the specified destination directory. The destination directory is created if it does + * not exist. If the destination file exists, then this method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destDir the directory to place the copy in, must not be null + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFile(File, File, boolean) + * @since Commons IO 1.3 + */ + public static void copyFileToDirectory(File srcFile, File destDir, boolean preserveFileDate) throws IOException { + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (destDir.exists() && destDir.isDirectory() == false) { + throw new IllegalArgumentException("Destination '" + destDir + "' is not a directory"); + } + copyFile(srcFile, new File(destDir, srcFile.getName()), preserveFileDate); + } + + /** + * Copies a file to a new location preserving the file date. + *

+ * This method copies the contents of the specified source file to the specified destination file. The directory holding the destination file is created if it does not exist. If + * the destination file exists, then this method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destFile the new file, must not be null + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFileToDirectory(File, File) + */ + public static void copyFile(File srcFile, File destFile) throws IOException { + copyFile(srcFile, destFile, true); + } + + /** + * Copies a file to a new location. + *

+ * This method copies the contents of the specified source file to the specified destination file. The directory holding the destination file is created if it does not exist. If + * the destination file exists, then this method will overwrite it. + * + * @param srcFile an existing file to copy, must not be null + * @param destFile the new file, must not be null + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFileToDirectory(File, File, boolean) + */ + public static void copyFile(File srcFile, File destFile, + boolean preserveFileDate) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (srcFile.exists() == false) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' exists but is a directory"); + } + if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath())) { + throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same"); + } + if (destFile.getParentFile() != null && destFile.getParentFile().exists() == false) { + if (destFile.getParentFile().mkdirs() == false) { + throw new IOException("Destination '" + destFile + "' directory cannot be created"); + } + } + if (destFile.exists() && destFile.canWrite() == false) { + throw new IOException("Destination '" + destFile + "' exists but is read-only"); + } + doCopyFile(srcFile, destFile, preserveFileDate); + } + + /** + * Internal copy file method. + * + * @param srcFile the validated source file, must not be null + * @param destFile the validated destination file, must not be null + * @param preserveFileDate whether to preserve the file date + * @throws IOException if an error occurs + */ + private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException { + if (destFile.exists() && destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' exists but is a directory"); + } + + FileInputStream input = new FileInputStream(srcFile); + try { + FileOutputStream output = new FileOutputStream(destFile); + try { + IOUtils.copy(input, output); + } + finally { + IOUtils.closeQuietly(output); + } + } + finally { + IOUtils.closeQuietly(input); + } + + if (srcFile.length() != destFile.length()) { + throw new IOException("Failed to copy full contents from '" + + srcFile + "' to '" + destFile + "'"); + } + if (preserveFileDate) { + destFile.setLastModified(srcFile.lastModified()); + } + } + + /** + * Copies a whole directory to a new location preserving the file dates. + *

+ * This method copies the specified directory and all its child directories and files to the specified destination. The destination is the new location and name of the directory. + *

+ * The destination directory is created if it does not exist. If the destination directory did exist, then this method merges the source with the destination, with the source + * taking precedence. + * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the new directory, must not be null + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.1 + */ + public static void copyDirectory(File srcDir, File destDir) throws IOException { + copyDirectory(srcDir, destDir, true); + } + + /** + * Copies a whole directory to a new location. + *

+ * This method copies the contents of the specified source directory to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. If the destination directory did exist, then this method merges the source with the destination, with the source + * taking precedence. + * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the new directory, must not be null + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.1 + */ + public static void copyDirectory(File srcDir, File destDir, + boolean preserveFileDate) throws IOException { + copyDirectory(srcDir, destDir, null, preserveFileDate); + } + + /** + * Copies a filtered directory to a new location. + *

+ * This method copies the contents of the specified source directory to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. If the destination directory did exist, then this method merges the source with the destination, with the source + * taking precedence. + * + *

Example: Copy directories only

+ * + *
+   * // only copy the directory structure
+   * FileUtils.copyDirectory(srcDir, destDir, DirectoryFileFilter.DIRECTORY, false);
+   * 
+ * + *

Example: Copy directories and txt files

+ * + *
+   * // Create a filter for ".txt" files
+   * IOFileFilter txtSuffixFilter = FileFilterUtils.suffixFileFilter(".txt");
+   * IOFileFilter txtFiles = FileFilterUtils.andFileFilter(FileFileFilter.FILE, txtSuffixFilter);
+   * 
+   * // Create a filter for either directories or ".txt" files
+   * FileFilter filter = FileFilterUtils.orFileFilter(DirectoryFileFilter.DIRECTORY, txtFiles);
+   * 
+   * // Copy using the filter
+   * FileUtils.copyDirectory(srcDir, destDir, filter, false);
+   * 
+ * + * @param srcDir an existing directory to copy, must not be null + * @param destDir the new directory, must not be null + * @param filter the filter to apply, null means copy all directories and files + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since Commons IO 1.4 + */ + public static void copyDirectory(File srcDir, File destDir, + FileFilter filter, boolean preserveFileDate) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (srcDir.exists() == false) { + throw new FileNotFoundException("Source '" + srcDir + "' does not exist"); + } + if (srcDir.isDirectory() == false) { + throw new IOException("Source '" + srcDir + "' exists but is not a directory"); + } + if (srcDir.getCanonicalPath().equals(destDir.getCanonicalPath())) { + throw new IOException("Source '" + srcDir + "' and destination '" + destDir + "' are the same"); + } + + // Cater for destination being directory within the source directory (see IO-141) + List exclusionList = null; + if (destDir.getCanonicalPath().startsWith(srcDir.getCanonicalPath())) { + File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter); + if (srcFiles != null && srcFiles.length > 0) { + exclusionList = new ArrayList(srcFiles.length); + for (int i = 0; i < srcFiles.length; i++) { + File copiedFile = new File(destDir, srcFiles[i].getName()); + exclusionList.add(copiedFile.getCanonicalPath()); + } + } + } + doCopyDirectory(srcDir, destDir, filter, preserveFileDate, exclusionList); + } + + /** + * Internal copy directory method. + * + * @param srcDir the validated source directory, must not be null + * @param destDir the validated destination directory, must not be null + * @param filter the filter to apply, null means copy all directories and files + * @param preserveFileDate whether to preserve the file date + * @param exclusionList List of files and directories to exclude from the copy, may be null + * @throws IOException if an error occurs + * @since Commons IO 1.1 + */ + private static void doCopyDirectory(File srcDir, File destDir, FileFilter filter, + boolean preserveFileDate, List exclusionList) throws IOException { + if (destDir.exists()) { + if (destDir.isDirectory() == false) { + throw new IOException("Destination '" + destDir + "' exists but is not a directory"); + } + } + else { + if (destDir.mkdirs() == false) { + throw new IOException("Destination '" + destDir + "' directory cannot be created"); + } + if (preserveFileDate) { + destDir.setLastModified(srcDir.lastModified()); + } + } + if (destDir.canWrite() == false) { + throw new IOException("Destination '" + destDir + "' cannot be written to"); + } + // recurse + File[] files = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + srcDir); + } + for (int i = 0; i < files.length; i++) { + File copiedFile = new File(destDir, files[i].getName()); + if (exclusionList == null || !exclusionList.contains(files[i].getCanonicalPath())) { + if (files[i].isDirectory()) { + doCopyDirectory(files[i], copiedFile, filter, preserveFileDate, exclusionList); + } + else { + doCopyFile(files[i], copiedFile, preserveFileDate); + } + } + } + } + + // ----------------------------------------------------------------------- + /** + * Deletes a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + */ + public static void deleteDirectory(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + cleanDirectory(directory); + if (!directory.delete()) { + String message = + "Unable to delete directory " + directory + "."; + throw new IOException(message); + } + } + + /** + * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • No exceptions are thrown when a file or directory cannot be deleted.
  • + *
+ * + * @param file file or directory to delete, can be null + * @return true if the file or directory was deleted, otherwise false + * + * @since Commons IO 1.4 + */ + public static boolean deleteQuietly(File file) { + if (file == null) { + return false; + } + try { + if (file.isDirectory()) { + cleanDirectory(file); + } + } + catch (Exception e) { + } + + try { + return file.delete(); + } + catch (Exception e) { + return false; + } + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean + * @throws IOException in case cleaning is unsuccessful + */ + public static void cleanDirectory(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (int i = 0; i < files.length; i++) { + File file = files[i]; + try { + forceDelete(file); + } + catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + // ----------------------------------------------------------------------- + /** + * Reads the contents of a file into a String. + * The file is always closed. + * + * @param file the file to read, must not be null + * @param encoding the encoding to use, null means platform default + * @return the file contents, never null + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + */ + public static String readFileToString(File file, String encoding) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.toString(in, encoding); + } + finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Reads the contents of a file into a String using the default encoding for the VM. + * The file is always closed. + * + * @param file the file to read, must not be null + * @return the file contents, never null + * @throws IOException in case of an I/O error + * @since Commons IO 1.3.1 + */ + public static String readFileToString(File file) throws IOException { + return readFileToString(file, null); + } + + // ----------------------------------------------------------------------- + /** + * Deletes a file. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • You get exceptions when a file or directory cannot be deleted. (java.io.File methods returns a boolean)
  • + *
+ * + * @param file file or directory to delete, must not be null + * @throws NullPointerException if the directory is null + * @throws FileNotFoundException if the file was not found + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDelete(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectory(file); + } + else { + boolean filePresent = file.exists(); + if (!file.delete()) { + if (!filePresent) { + throw new FileNotFoundException("File does not exist: " + file); + } + String message = + "Unable to delete file: " + file; + throw new IOException(message); + } + } + } + + /** + * Schedules a file to be deleted when JVM exits. + * If file is directory delete it and all sub-directories. + * + * @param file file or directory to delete, must not be null + * @throws NullPointerException if the file is null + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDeleteOnExit(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectoryOnExit(file); + } + else { + file.deleteOnExit(); + } + } + + /** + * Schedules a directory recursively for deletion on JVM exit. + * + * @param directory directory to delete, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException in case deletion is unsuccessful + */ + private static void deleteDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + cleanDirectoryOnExit(directory); + directory.deleteOnExit(); + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException in case cleaning is unsuccessful + */ + private static void cleanDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (int i = 0; i < files.length; i++) { + File file = files[i]; + try { + forceDeleteOnExit(file); + } + catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + /** + * Makes a directory, including any necessary but nonexistent parent + * directories. If there already exists a file with specified name or + * the directory cannot be created then an exception is thrown. + * + * @param directory directory to create, must not be null + * @throws NullPointerException if the directory is null + * @throws IOException if the directory cannot be created + */ + public static void forceMkdir(File directory) throws IOException { + if (directory.exists()) { + if (directory.isFile()) { + String message = + "File " + + directory + + " exists and is " + + "not a directory. Unable to create directory."; + throw new IOException(message); + } + } + else { + if (!directory.mkdirs()) { + String message = + "Unable to create directory " + directory; + throw new IOException(message); + } + } + } + + // ----------------------------------------------------------------------- + /** + * Counts the size of a directory recursively (sum of the length of all files). + * + * @param directory directory to inspect, must not be null + * @return size of directory in bytes, 0 if directory is security restricted + * @throws NullPointerException if the directory is null + */ + public static long sizeOfDirectory(File directory) { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + long size = 0; + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + return 0L; + } + for (int i = 0; i < files.length; i++) { + File file = files[i]; + + if (file.isDirectory()) { + size += sizeOfDirectory(file); + } + else { + size += file.length(); + } + } + + return size; + } + + /** + * Moves a directory. + *

+ * When the destination directory is on another file system, do a "copy and delete". + * + * @param srcDir the directory to be moved + * @param destDir the destination directory + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since Commons IO 1.4 + */ + public static void moveDirectory(File srcDir, File destDir) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcDir.exists()) { + throw new FileNotFoundException("Source '" + srcDir + "' does not exist"); + } + if (!srcDir.isDirectory()) { + throw new IOException("Source '" + srcDir + "' is not a directory"); + } + if (destDir.exists()) { + throw new IOException("Destination '" + destDir + "' already exists"); + } + boolean rename = srcDir.renameTo(destDir); + if (!rename) { + copyDirectory(srcDir, destDir); + deleteDirectory(srcDir); + if (srcDir.exists()) { + throw new IOException("Failed to delete original directory '" + srcDir + + "' after copy to '" + destDir + "'"); + } + } + } + + /** + * Moves a file. + *

+ * When the destination file is on another file system, do a "copy and delete". + * + * @param srcFile the file to be moved + * @param destFile the destination file + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since Commons IO 1.4 + */ + public static void moveFile(File srcFile, File destFile) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcFile.exists()) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' is a directory"); + } + if (destFile.exists()) { + throw new IOException("Destination '" + destFile + "' already exists"); + } + if (destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' is a directory"); + } + boolean rename = srcFile.renameTo(destFile); + if (!rename) { + copyFile(srcFile, destFile); + if (!srcFile.delete()) { + FileUtils.deleteQuietly(destFile); + throw new IOException("Failed to delete original file '" + srcFile + + "' after copy to '" + destFile + "'"); + } + } + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/commons/FilenameUtils.java b/app/src/main/java/org/zeroturnaround/zip/commons/FilenameUtils.java new file mode 100644 index 0000000..dcd3bcf --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/commons/FilenameUtils.java @@ -0,0 +1,231 @@ +/* + * 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.zeroturnaround.zip.commons; + +import java.io.File; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache commons-io package. All license and other documentation is intact. + * + * General filename and filepath manipulation utilities. + *

+ * When dealing with filenames you can hit problems when moving from a Windows based development machine to a Unix based production machine. This class aims to help avoid those + * problems. + *

+ * NOTE: You may be able to avoid using this class entirely simply by using JDK {@link java.io.File File} objects and the two argument constructor + * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}. + *

+ * Most methods on this class are designed to work the same on both Unix and Windows. Those that don't include 'System', 'Unix' or 'Windows' in their name. + *

+ * Most methods recognise both separators (forward and back), and both sets of prefixes. See the javadoc of each method for details. + *

+ * This class defines six components within a filename (example C:\dev\project\file.txt): + *

    + *
  • the prefix - C:\
  • + *
  • the path - dev\project\
  • + *
  • the full path - C:\dev\project\
  • + *
  • the name - file.txt
  • + *
  • the base name - file
  • + *
  • the extension - txt
  • + *
+ * Note that this class works best if directory filenames end with a separator. If you omit the last separator, it is impossible to determine if the filename corresponds to a file + * or a directory. As a result, we have chosen to say it corresponds to a file. + *

+ * This class only supports Unix and Windows style names. Prefixes are matched as follows: + * + *

+ * Windows:
+ * a\b\c.txt           --> ""          --> relative
+ * \a\b\c.txt          --> "\"         --> current drive absolute
+ * C:a\b\c.txt         --> "C:"        --> drive relative
+ * C:\a\b\c.txt        --> "C:\"       --> absolute
+ * \\server\a\b\c.txt  --> "\\server\" --> UNC
+ * 
+ * Unix:
+ * a/b/c.txt           --> ""          --> relative
+ * /a/b/c.txt          --> "/"         --> absolute
+ * ~/a/b/c.txt         --> "~/"        --> current user
+ * ~                   --> "~/"        --> current user (slash added)
+ * ~user/a/b/c.txt     --> "~user/"    --> named user
+ * ~user               --> "~user/"    --> named user (slash added)
+ * 
+ * + * Both prefix styles are matched always, irrespective of the machine that you are currently running on. + *

+ * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils. + * + * @author Kevin A. Burton + * @author Scott Sanders + * @author Daniel Rall + * @author Christoph.Reck + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Martin Cooper + * @author Jeremias Maerki + * @author Stephen Colebourne + * @version $Id: FilenameUtils.java 609870 2008-01-08 04:46:26Z niallp $ + * @since Commons IO 1.1 + */ +public class FilenameUtils { + + /** + * The extension separator character. + * + * @since Commons IO 1.4 + */ + public static final char EXTENSION_SEPARATOR = '.'; + + /** + * The extension separator String. + * + * @since Commons IO 1.4 + */ + public static final String EXTENSION_SEPARATOR_STR = (Character.valueOf(EXTENSION_SEPARATOR)).toString(); + + /** + * The Unix separator character. + */ + private static final char UNIX_SEPARATOR = '/'; + + /** + * The Windows separator character. + */ + private static final char WINDOWS_SEPARATOR = '\\'; + + /** + * The system separator character. + */ + private static final char SYSTEM_SEPARATOR = File.separatorChar; + + /** + * Instances should NOT be constructed in standard programming. + */ + public FilenameUtils() { + super(); + } + + // ----------------------------------------------------------------------- + /** + * Determines if Windows file system is in use. + * + * @return true if the system is Windows + */ + static boolean isSystemWindows() { + return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR; + } + + // ----------------------------------------------------------------------- + /** + * Checks if the character is a separator. + * + * @param ch the character to check + * @return true if it is a separator character + */ + private static boolean isSeparator(char ch) { + return (ch == UNIX_SEPARATOR) || (ch == WINDOWS_SEPARATOR); + } + + // ----------------------------------------------------------------------- + /** + * Returns the length of the filename prefix, such as C:/ or ~/. + *

+ * This method will handle a file in either Unix or Windows format. + *

+ * The prefix length includes the first slash in the full filename if applicable. Thus, it is possible that the length returned is greater than the length of the input string. + * + *

+   * Windows:
+   * a\b\c.txt           --> ""          --> relative
+   * \a\b\c.txt          --> "\"         --> current drive absolute
+   * C:a\b\c.txt         --> "C:"        --> drive relative
+   * C:\a\b\c.txt        --> "C:\"       --> absolute
+   * \\server\a\b\c.txt  --> "\\server\" --> UNC
+   * 
+   * Unix:
+   * a/b/c.txt           --> ""          --> relative
+   * /a/b/c.txt          --> "/"         --> absolute
+   * ~/a/b/c.txt         --> "~/"        --> current user
+   * ~                   --> "~/"        --> current user (slash added)
+   * ~user/a/b/c.txt     --> "~user/"    --> named user
+   * ~user               --> "~user/"    --> named user (slash added)
+   * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. ie. both Unix and Windows prefixes are matched regardless. + * + * @param filename the filename to find the prefix in, null returns -1 + * @return the length of the prefix, -1 if invalid or null + */ + public static int getPrefixLength(String filename) { + if (filename == null) { + return -1; + } + int len = filename.length(); + if (len == 0) { + return 0; + } + char ch0 = filename.charAt(0); + if (ch0 == ':') { + return -1; + } + if (len == 1) { + if (ch0 == '~') { + return 2; // return a length greater than the input + } + return (isSeparator(ch0) ? 1 : 0); + } + else { + if (ch0 == '~') { + int posUnix = filename.indexOf(UNIX_SEPARATOR, 1); + int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1); + if (posUnix == -1 && posWin == -1) { + return len + 1; // return a length greater than the input + } + posUnix = (posUnix == -1 ? posWin : posUnix); + posWin = (posWin == -1 ? posUnix : posWin); + return Math.min(posUnix, posWin) + 1; + } + char ch1 = filename.charAt(1); + if (ch1 == ':') { + ch0 = Character.toUpperCase(ch0); + if (ch0 >= 'A' && ch0 <= 'Z') { + if (len == 2 || isSeparator(filename.charAt(2)) == false) { + return 2; + } + return 3; + } + return -1; + + } + else if (isSeparator(ch0) && isSeparator(ch1)) { + int posUnix = filename.indexOf(UNIX_SEPARATOR, 2); + int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2); + if ((posUnix == -1 && posWin == -1) || posUnix == 2 || posWin == 2) { + return -1; + } + posUnix = (posUnix == -1 ? posWin : posUnix); + posWin = (posWin == -1 ? posUnix : posWin); + return Math.min(posUnix, posWin) + 1; + } + else { + return (isSeparator(ch0) ? 1 : 0); + } + } + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/commons/IOUtils.java b/app/src/main/java/org/zeroturnaround/zip/commons/IOUtils.java new file mode 100644 index 0000000..5514ff9 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/commons/IOUtils.java @@ -0,0 +1,373 @@ +/* + * 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.zeroturnaround.zip.commons; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache commons-io package. All license and other documentation is intact. + * + * General IO stream manipulation utilities. + *

+ * This class provides static utility methods for input/output operations. + *

    + *
  • closeQuietly - these methods close a stream ignoring nulls and exceptions + *
  • toXxx/read - these methods read data from a stream + *
  • write - these methods write data to a stream + *
  • copy - these methods copy all the data from one stream to another + *
  • contentEquals - these methods compare the content of two streams + *
+ *

+ * The byte-to-char methods and char-to-byte methods involve a conversion step. Two methods are provided in each case, one that uses the platform default encoding and the other + * which allows you to specify an encoding. You are encouraged to always specify an encoding because relying on the platform default can lead to unexpected results, for example + * when moving from development to production. + *

+ * All the methods in this class that read a stream are buffered internally. This means that there is no cause to use a BufferedInputStream or + * BufferedReader. The default buffer size of 4K has been shown to be efficient in tests. + *

+ * Wherever possible, the methods in this class do not flush or close the stream. This is to avoid making non-portable assumptions about the streams' origin and further + * use. Thus the caller is still responsible for closing streams after use. + *

+ * Origin of code: Excalibur. + * + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Stephen Colebourne + * @author Gareth Davis + * @author Ian Springer + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id: IOUtils.java 481854 2006-12-03 18:30:07Z scolebourne $ + */ +public class IOUtils { + // NOTE: This class is focused on InputStream, OutputStream, Reader and + // Writer. Each method should take at least one of these as a parameter, + // or return one of them. + + /** + * The Unix directory separator character. + */ + public static final char DIR_SEPARATOR_UNIX = '/'; + /** + * The Windows directory separator character. + */ + public static final char DIR_SEPARATOR_WINDOWS = '\\'; + /** + * The system directory separator character. + */ + public static final char DIR_SEPARATOR = File.separatorChar; + /** + * The Unix line separator string. + */ + public static final String LINE_SEPARATOR_UNIX = "\n"; + /** + * The Windows line separator string. + */ + public static final String LINE_SEPARATOR_WINDOWS = "\r\n"; + /** + * The system line separator string. + */ + public static final String LINE_SEPARATOR; + static { + // avoid security issues + StringWriter buf = new StringWriter(4); + PrintWriter out = new PrintWriter(buf); + out.println(); + LINE_SEPARATOR = buf.toString(); + } + + /** + * The default buffer size to use. + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Instances should NOT be constructed in standard programming. + */ + public IOUtils() { + super(); + } + + /** + * Unconditionally close an InputStream. + *

+ * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored. This is typically used in finally blocks. + * + * @param input the InputStream to close, may be null or already closed + */ + public static void closeQuietly(InputStream input) { + try { + if (input != null) { + input.close(); + } + } + catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close an OutputStream. + *

+ * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored. This is typically used in finally blocks. + * + * @param output the OutputStream to close, may be null or already closed + */ + public static void closeQuietly(OutputStream output) { + try { + if (output != null) { + output.close(); + } + } + catch (IOException ioe) { + // ignore + } + } + + // read toByteArray + // ----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + /** + * Get the contents of an InputStream as a String + * using the specified character encoding. + *

+ * Character encoding names can be found at IANA. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + * + * @param input the InputStream to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(InputStream input, String encoding) + throws IOException { + StringWriter sw = new StringWriter(); + copy(input, sw, encoding); + return sw.toString(); + } + + // copy from InputStream + // ----------------------------------------------------------------------- + /** + * Copy bytes from an InputStream to an OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + *

+ * Large streams (over 2GB) will return a bytes copied value of -1 after the copy has completed since the correct number of bytes cannot be returned as an int. For + * large streams use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws ArithmeticException if the byte count is too large + * @since Commons IO 1.1 + */ + public static int copy(InputStream input, OutputStream output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.3 + */ + public static long copyLarge(InputStream input, OutputStream output) + throws IOException { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy bytes from an InputStream to chars on a Writer using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(InputStream input, Writer output) + throws IOException { + InputStreamReader in = new InputStreamReader(input); + copy(in, output); + } + + /** + * Copy bytes from an InputStream to chars on a Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + *

+ * Character encoding names can be found at IANA. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(InputStream input, Writer output, String encoding) + throws IOException { + if (encoding == null) { + copy(input, output); + } + else { + InputStreamReader in = new InputStreamReader(input, encoding); + copy(in, output); + } + } + + // copy from Reader + // ----------------------------------------------------------------------- + /** + * Copy chars from a Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a BufferedReader. + *

+ * Large streams (over 2GB) will return a chars copied value of -1 after the copy has completed since the correct number of chars cannot be returned as an int. For + * large streams use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws ArithmeticException if the character count is too large + * @since Commons IO 1.1 + */ + public static int copy(Reader input, Writer output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy chars from a large (over 2GB) Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a BufferedReader. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.3 + */ + public static long copyLarge(Reader input, Writer output) throws IOException { + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + // content equals + // ----------------------------------------------------------------------- + /** + * Compare the contents of two Streams to determine if they are equal or + * not. + *

+ * This method buffers the input internally using BufferedInputStream if they are not already buffered. + * + * @param input1 the first stream + * @param input2 the second stream + * @return true if the content of the streams are equal or they both don't + * exist, false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + */ + public static boolean contentEquals(InputStream input1, InputStream input2) + throws IOException { + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return (ch2 == -1); + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/extra/AsiExtraField.java b/app/src/main/java/org/zeroturnaround/zip/extra/AsiExtraField.java new file mode 100644 index 0000000..42b1098 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/extra/AsiExtraField.java @@ -0,0 +1,433 @@ +/* + * 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.zeroturnaround.zip.extra; + +import java.util.zip.CRC32; +import java.util.zip.ZipException; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package. + * All license and other documentation is intact. + * + * Adds Unix file permission and UID/GID fields as well as symbolic + * link handling. + * + *

+ * This class uses the ASi extra field in the format: + *

+ * + *
+ *         Value         Size            Description
+ *         -----         ----            -----------
+ * (Unix3) 0x756e        Short           tag for this extra block type
+ *         TSize         Short           total data size for this block
+ *         CRC           Long            CRC-32 of the remaining data
+ *         Mode          Short           file permissions
+ *         SizDev        Long            symlink'd size OR major/minor dev num
+ *         UID           Short           user ID
+ *         GID           Short           group ID
+ *         (var.)        variable        symbolic link filename
+ * 
+ * + *

+ * taken from appnote.iz (Info-ZIP note, 981119) found at ftp://ftp.uu.net/pub/archiving/zip/doc/ + *

+ * + * + *

+ * Short is two bytes and Long is four bytes in big endian byte and word order, device numbers are currently not supported. + *

+ * + *

+ * Since the documentation this class is based upon doesn't mention the character encoding of the file name at all, it is assumed that it uses the current platform's default + * encoding. + *

+ */ +public class AsiExtraField implements ZipExtraField, Cloneable { + + /** + * Bits used for permissions (and sticky bit) + * + * @since 1.1 + */ + final int PERM_MASK = 07777; + /** + * Indicates symbolic links. + * + * @since 1.1 + */ + final int LINK_FLAG = 0120000; + /** + * Indicates plain files. + * + * @since 1.1 + */ + final int FILE_FLAG = 0100000; + /** + * Indicates directories. + * + * @since 1.1 + */ + final int DIR_FLAG = 040000; + + // ---------------------------------------------------------- + // somewhat arbitrary choices that are quite common for shared + // installations + // ----------------------------------------------------------- + + /** + * Default permissions for symbolic links. + * + * @since 1.1 + */ + final int DEFAULT_LINK_PERM = 0777; + /** + * Default permissions for directories. + * + * @since 1.1 + */ + final int DEFAULT_DIR_PERM = 0755; + /** + * Default permissions for plain files. + * + * @since 1.1 + */ + final int DEFAULT_FILE_PERM = 0644; + + private static final ZipShort HEADER_ID = new ZipShort(0x756E); + private static final int WORD = 4; + /** + * Standard Unix stat(2) file mode. + * + * @since 1.1 + */ + private int mode = 0; + /** + * User ID. + * + * @since 1.1 + */ + private int uid = 0; + /** + * Group ID. + * + * @since 1.1 + */ + private int gid = 0; + /** + * File this entry points to, if it is a symbolic link. + * + *

+ * empty string - if entry is not a symbolic link. + *

+ * + * @since 1.1 + */ + private String link = ""; + /** + * Is this an entry for a directory? + * + * @since 1.1 + */ + private boolean dirFlag = false; + + /** + * Instance used to calculate checksums. + * + * @since 1.1 + */ + private CRC32 crc = new CRC32(); + + /** Constructor for AsiExtraField. */ + public AsiExtraField() { + } + + /** + * The Header-ID. + * + * @return the value for the header id for this extrafield + * @since 1.1 + */ + public ZipShort getHeaderId() { + return HEADER_ID; + } + + /** + * Length of the extra field in the local file data - without + * Header-ID or length specifier. + * + * @return a ZipShort for the length of the data of this extra field + * @since 1.1 + */ + public ZipShort getLocalFileDataLength() { + return new ZipShort(WORD // CRC + + 2 // Mode + + WORD // SizDev + + 2 // UID + + 2 // GID + + getLinkedFile().getBytes().length); + // Uses default charset - see class Javadoc + } + + /** + * Delegate to local file data. + * + * @return the centralDirectory length + * @since 1.1 + */ + public ZipShort getCentralDirectoryLength() { + return getLocalFileDataLength(); + } + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return get the data + * @since 1.1 + */ + public byte[] getLocalFileDataData() { + // CRC will be added later + byte[] data = new byte[getLocalFileDataLength().getValue() - WORD]; + System.arraycopy(ZipShort.getBytes(getMode()), 0, data, 0, 2); + + byte[] linkArray = getLinkedFile().getBytes(); // Uses default charset - see class Javadoc + // CheckStyle:MagicNumber OFF + System.arraycopy(ZipLong.getBytes(linkArray.length), + 0, data, 2, WORD); + + System.arraycopy(ZipShort.getBytes(getUserId()), + 0, data, 6, 2); + System.arraycopy(ZipShort.getBytes(getGroupId()), + 0, data, 8, 2); + + System.arraycopy(linkArray, 0, data, 10, linkArray.length); + // CheckStyle:MagicNumber ON + + crc.reset(); + crc.update(data); + long checksum = crc.getValue(); + + byte[] result = new byte[data.length + WORD]; + System.arraycopy(ZipLong.getBytes(checksum), 0, result, 0, WORD); + System.arraycopy(data, 0, result, WORD, data.length); + return result; + } + + /** + * Delegate to local file data. + * + * @return the local file data + * @since 1.1 + */ + public byte[] getCentralDirectoryData() { + return getLocalFileDataData(); + } + + /** + * Set the user id. + * + * @param uid the user id + * @since 1.1 + */ + public void setUserId(int uid) { + this.uid = uid; + } + + /** + * Get the user id. + * + * @return the user id + * @since 1.1 + */ + public int getUserId() { + return uid; + } + + /** + * Set the group id. + * + * @param gid the group id + * @since 1.1 + */ + public void setGroupId(int gid) { + this.gid = gid; + } + + /** + * Get the group id. + * + * @return the group id + * @since 1.1 + */ + public int getGroupId() { + return gid; + } + + /** + * Indicate that this entry is a symbolic link to the given filename. + * + * @param name Name of the file this entry links to, empty String + * if it is not a symbolic link. + * + * @since 1.1 + */ + public void setLinkedFile(String name) { + link = name; + mode = getMode(mode); + } + + /** + * Name of linked file + * + * @return name of the file this entry links to if it is a + * symbolic link, the empty string otherwise. + * + * @since 1.1 + */ + public String getLinkedFile() { + return link; + } + + /** + * Is this entry a symbolic link? + * + * @return true if this is a symbolic link + * @since 1.1 + */ + public boolean isLink() { + return getLinkedFile().length() != 0; + } + + /** + * File mode of this file. + * + * @param mode the file mode + * @since 1.1 + */ + public void setMode(int mode) { + this.mode = getMode(mode); + } + + /** + * File mode of this file. + * + * @return the file mode + * @since 1.1 + */ + public int getMode() { + return mode; + } + + /** + * Indicate whether this entry is a directory. + * + * @param dirFlag if true, this entry is a directory + * @since 1.1 + */ + public void setDirectory(boolean dirFlag) { + this.dirFlag = dirFlag; + mode = getMode(mode); + } + + /** + * Is this entry a directory? + * + * @return true if this entry is a directory + * @since 1.1 + */ + public boolean isDirectory() { + return dirFlag && !isLink(); + } + + /** + * Populate data from this array as if it was in local file data. + * + * @param data an array of bytes + * @param offset the start offset + * @param length the number of bytes in the array from offset + * @since 1.1 + * @throws ZipException on error + */ + public void parseFromLocalFileData(byte[] data, int offset, int length) + throws ZipException { + + long givenChecksum = ZipLong.getValue(data, offset); + byte[] tmp = new byte[length - WORD]; + System.arraycopy(data, offset + WORD, tmp, 0, length - WORD); + crc.reset(); + crc.update(tmp); + long realChecksum = crc.getValue(); + if (givenChecksum != realChecksum) { + throw new ZipException("bad CRC checksum " + + Long.toHexString(givenChecksum) + + " instead of " + + Long.toHexString(realChecksum)); + } + + int newMode = ZipShort.getValue(tmp, 0); + // CheckStyle:MagicNumber OFF + byte[] linkArray = new byte[(int) ZipLong.getValue(tmp, 2)]; + uid = ZipShort.getValue(tmp, 6); + gid = ZipShort.getValue(tmp, 8); + + if (linkArray.length == 0) { + link = ""; + } + else { + System.arraycopy(tmp, 10, linkArray, 0, linkArray.length); + link = new String(linkArray); // Uses default charset - see class Javadoc + } + // CheckStyle:MagicNumber ON + setDirectory((newMode & DIR_FLAG) != 0); + setMode(newMode); + } + + /** + * Get the file mode for given permissions with the correct file type. + * + * @param mode the mode + * @return the type with the mode + * @since 1.1 + */ + protected int getMode(int mode) { + int type = FILE_FLAG; + if (isLink()) { + type = LINK_FLAG; + } + else if (isDirectory()) { + type = DIR_FLAG; + } + return type | (mode & PERM_MASK); + } + + @Override + public Object clone() { + try { + AsiExtraField cloned = (AsiExtraField) super.clone(); + cloned.crc = new CRC32(); + return cloned; + } + catch (CloneNotSupportedException cnfe) { + // impossible + throw new RuntimeException(cnfe); + } + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/extra/ExtraFieldUtils.java b/app/src/main/java/org/zeroturnaround/zip/extra/ExtraFieldUtils.java new file mode 100644 index 0000000..1471527 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/extra/ExtraFieldUtils.java @@ -0,0 +1,175 @@ +/* + * 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.zeroturnaround.zip.extra; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.ZipException; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package. + * All license and other documentation is intact. + * + * ZipExtraField related methods + * + */ +// CheckStyle:HideUtilityClassConstructorCheck OFF (bc) +public class ExtraFieldUtils { + + private static final int WORD = 4; + + /** + * Static registry of known extra fields. + * + * @since 1.1 + */ + private static final Map> implementations; + + static { + implementations = new ConcurrentHashMap>(); + register(AsiExtraField.class); + } + + /** + * Register a ZipExtraField implementation. + * + *

+ * The given class must have a no-arg constructor and implement the {@link ZipExtraField ZipExtraField interface}. + *

+ * + * @param c the class to register + * + * @since 1.1 + */ + public static void register(Class c) { + try { + ZipExtraField ze = (ZipExtraField) c.newInstance(); + implementations.put(ze.getHeaderId(), c); + } + catch (ClassCastException cc) { + throw new RuntimeException(c + " doesn\'t implement ZipExtraField"); + } + catch (InstantiationException ie) { + throw new RuntimeException(c + " is not a concrete class"); + } + catch (IllegalAccessException ie) { + throw new RuntimeException(c + "\'s no-arg constructor is not public"); + } + } + + /** + * Create an instance of the appropriate ExtraField, falls back to {@link UnrecognizedExtraField UnrecognizedExtraField}. + * + * @param headerId the header identifier + * @return an instance of the appropriate ExtraField + * @exception InstantiationException if unable to instantiate the class + * @exception IllegalAccessException if not allowed to instantiate the class + * @since 1.1 + */ + public static ZipExtraField createExtraField(ZipShort headerId) + throws InstantiationException, IllegalAccessException { + Class c = implementations.get(headerId); + if (c != null) { + return (ZipExtraField) c.newInstance(); + } + UnrecognizedExtraField u = new UnrecognizedExtraField(); + u.setHeaderId(headerId); + return u; + } + + public static ZipExtraField[] parseA(byte[] data) throws ZipException { + List v = parse(data); + ZipExtraField[] result = new ZipExtraField[v.size()]; + return v.toArray(result); + } + + /** + * Split the array into ExtraFields and populate them with the + * given data as local file data, throwing an exception if the + * data cannot be parsed. + * + * @param data an array of bytes as it appears in local file data + * @return an array of ExtraFields + * @throws ZipException on error + */ + public static List parse(byte[] data) throws ZipException { + List v = new ArrayList(); + if (data == null) { + return v; + } + int start = 0; + while (start <= data.length - WORD) { + ZipShort headerId = new ZipShort(data, start); + int length = (new ZipShort(data, start + 2)).getValue(); + if (start + WORD + length > data.length) { + throw new ZipException("bad extra field starting at " + + start + ". Block length of " + + length + " bytes exceeds remaining" + + " data of " + + (data.length - start - WORD) + + " bytes."); + } + try { + ZipExtraField ze = createExtraField(headerId); + ze.parseFromLocalFileData(data, start + WORD, length); + v.add(ze); + } + catch (InstantiationException ie) { + throw new ZipException(ie.getMessage()); + } + catch (IllegalAccessException iae) { + throw new ZipException(iae.getMessage()); + } + start += (length + WORD); + } + return v; + } + + /** + * Merges the local file data fields of the given ZipExtraFields. + * + * @param data an array of ExtraFiles + * @return an array of bytes + * @since 1.1 + */ + public static byte[] mergeLocalFileDataData(List data) { + int regularExtraFieldCount = data.size(); + + int sum = WORD * regularExtraFieldCount; + for (ZipExtraField element : data) { + sum += element.getLocalFileDataLength().getValue(); + } + + byte[] result = new byte[sum]; + int start = 0; + for (ZipExtraField element : data) { + System.arraycopy(element.getHeaderId().getBytes(), + 0, result, start, 2); + System.arraycopy(element.getLocalFileDataLength().getBytes(), + 0, result, start + 2, 2); + byte[] local = element.getLocalFileDataData(); + System.arraycopy(local, 0, result, start + WORD, local.length); + start += (local.length + WORD); + } + return result; + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/extra/UnrecognizedExtraField.java b/app/src/main/java/org/zeroturnaround/zip/extra/UnrecognizedExtraField.java new file mode 100644 index 0000000..db49585 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/extra/UnrecognizedExtraField.java @@ -0,0 +1,178 @@ +/* + * 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.zeroturnaround.zip.extra; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package. + * All license and other documentation is intact. + * + * Simple placeholder for all those extra fields we don't want to deal + * with. + * + *

+ * Assumes local file data and central directory entries are identical - unless told the opposite. + *

+ * + */ +public class UnrecognizedExtraField implements ZipExtraField { + + /** + * The Header-ID. + * + * @since 1.1 + */ + private ZipShort headerId; + + /** + * Set the header id. + * + * @param headerId the header id to use + */ + public void setHeaderId(ZipShort headerId) { + this.headerId = headerId; + } + + /** + * Get the header id. + * + * @return the header id + */ + public ZipShort getHeaderId() { + return headerId; + } + + /** + * Extra field data in local file data - without + * Header-ID or length specifier. + * + * @since 1.1 + */ + private byte[] localData; + + /** + * Set the extra field data in the local file data - + * without Header-ID or length specifier. + * + * @param data the field data to use + */ + public void setLocalFileDataData(byte[] data) { + localData = copy(data); + } + + /** + * Get the length of the local data. + * + * @return the length of the local data + */ + public ZipShort getLocalFileDataLength() { + return new ZipShort(localData.length); + } + + /** + * Get the local data. + * + * @return the local data + */ + public byte[] getLocalFileDataData() { + return copy(localData); + } + + /** + * Extra field data in central directory - without + * Header-ID or length specifier. + * + * @since 1.1 + */ + private byte[] centralData; + + /** + * Set the extra field data in central directory. + * + * @param data the data to use + */ + public void setCentralDirectoryData(byte[] data) { + centralData = copy(data); + } + + /** + * Get the central data length. + * If there is no central data, get the local file data length. + * + * @return the central data length + */ + public ZipShort getCentralDirectoryLength() { + if (centralData != null) { + return new ZipShort(centralData.length); + } + return getLocalFileDataLength(); + } + + /** + * Get the central data. + * + * @return the central data if present, else return the local file data + */ + public byte[] getCentralDirectoryData() { + if (centralData != null) { + return copy(centralData); + } + return getLocalFileDataData(); + } + + /** + * @param data the array of bytes. + * @param offset the source location in the data array. + * @param length the number of bytes to use in the data array. + * @see ZipExtraField#parseFromLocalFileData(byte[], int, int) + */ + public void parseFromLocalFileData(byte[] data, int offset, int length) { + byte[] tmp = new byte[length]; + System.arraycopy(data, offset, tmp, 0, length); + setLocalFileDataData(tmp); + } + + /** + * @param data the array of bytes. + * @param offset the source location in the data array. + * @param length the number of bytes to use in the data array. + */ + public void parseFromCentralDirectoryData(byte[] data, int offset, + int length) { + byte[] tmp = new byte[length]; + System.arraycopy(data, offset, tmp, 0, length); + setCentralDirectoryData(tmp); + if (localData == null) { + setLocalFileDataData(tmp); + } + } + + /** + * Create a copy of the given array - or return null if the + * argument is null. + */ + private static byte[] copy(byte[] from) { + if (from != null) { + byte[] to = new byte[from.length]; + System.arraycopy(from, 0, to, 0, to.length); + return to; + } + return null; + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/extra/ZipConstants.java b/app/src/main/java/org/zeroturnaround/zip/extra/ZipConstants.java new file mode 100644 index 0000000..f820f78 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/extra/ZipConstants.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.zeroturnaround.zip.extra; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package. + * All license and other documentation is intact. + * + * Various constants used throughout the package. + */ +final class ZipConstants { + private ZipConstants() { + } + + /** Masks last eight bits */ + static final int BYTE_MASK = 0xFF; + + /** length of a ZipShort in bytes */ + static final int SHORT = 2; + + /** length of a ZipLong in bytes */ + static final int WORD = 4; + + /** length of a ZipEightByteInteger in bytes */ + static final int DWORD = 8; + + /** Initial ZIP specification version */ + static final int INITIAL_VERSION = 10; + + /** ZIP specification version that introduced data descriptor method */ + static final int DATA_DESCRIPTOR_MIN_VERSION = 20; + + /** ZIP specification version that introduced ZIP64 */ + static final int ZIP64_MIN_VERSION = 45; + + /** + * Value stored in two-byte size and similar fields if ZIP64 + * extensions are used. + */ + static final int ZIP64_MAGIC_SHORT = 0xFFFF; + + /** + * Value stored in four-byte size and similar fields if ZIP64 + * extensions are used. + */ + static final long ZIP64_MAGIC = 0xFFFFFFFFL; + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/extra/ZipExtraField.java b/app/src/main/java/org/zeroturnaround/zip/extra/ZipExtraField.java new file mode 100644 index 0000000..bce0f36 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/extra/ZipExtraField.java @@ -0,0 +1,94 @@ +/* + * 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.zeroturnaround.zip.extra; + +import java.util.zip.ZipException; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package. + * All license and other documentation is intact. + * + * General format of extra field data. + * + *

+ * Extra fields usually appear twice per file, once in the local file data and once in the central directory. Usually they are the same, but they don't have to be. + * {@link java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} will only use the local file data in both places. + *

+ * + */ +public interface ZipExtraField { + + /** + * The Header-ID. + * + * @return the header id + * @since 1.1 + */ + ZipShort getHeaderId(); + + /** + * Length of the extra field in the local file data - without + * Header-ID or length specifier. + * + * @return the length of the field in the local file data + * @since 1.1 + */ + ZipShort getLocalFileDataLength(); + + /** + * Length of the extra field in the central directory - without + * Header-ID or length specifier. + * + * @return the length of the field in the central directory + * @since 1.1 + */ + ZipShort getCentralDirectoryLength(); + + /** + * The actual data to put into local file data - without Header-ID + * or length specifier. + * + * @return the data + * @since 1.1 + */ + byte[] getLocalFileDataData(); + + /** + * The actual data to put into central directory - without Header-ID or + * length specifier. + * + * @return the data + * @since 1.1 + */ + byte[] getCentralDirectoryData(); + + /** + * Populate data from this array as if it was in local file data. + * + * @param data an array of bytes + * @param offset the start offset + * @param length the number of bytes in the array from offset + * + * @since 1.1 + * @throws ZipException on error + */ + void parseFromLocalFileData(byte[] data, int offset, int length) + throws ZipException; +} diff --git a/app/src/main/java/org/zeroturnaround/zip/extra/ZipLong.java b/app/src/main/java/org/zeroturnaround/zip/extra/ZipLong.java new file mode 100644 index 0000000..35d465e --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/extra/ZipLong.java @@ -0,0 +1,200 @@ +/* + * 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.zeroturnaround.zip.extra; + +import static org.zeroturnaround.zip.extra.ZipConstants.BYTE_MASK; +import static org.zeroturnaround.zip.extra.ZipConstants.WORD; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package. + * All license and other documentation is intact. + * + * Utility class that represents a four byte integer with conversion + * rules for the big endian byte order of ZIP files. + * + */ +public final class ZipLong implements Cloneable { + + // private static final int BYTE_BIT_SIZE = 8; + + private static final int BYTE_1 = 1; + private static final int BYTE_1_MASK = 0xFF00; + private static final int BYTE_1_SHIFT = 8; + + private static final int BYTE_2 = 2; + private static final int BYTE_2_MASK = 0xFF0000; + private static final int BYTE_2_SHIFT = 16; + + private static final int BYTE_3 = 3; + private static final long BYTE_3_MASK = 0xFF000000L; + private static final int BYTE_3_SHIFT = 24; + + private final long value; + + /** Central File Header Signature */ + public static final ZipLong CFH_SIG = new ZipLong(0X02014B50L); + + /** Local File Header Signature */ + public static final ZipLong LFH_SIG = new ZipLong(0X04034B50L); + + /** + * Data Descriptor signature + */ + public static final ZipLong DD_SIG = new ZipLong(0X08074B50L); + + /** + * Value stored in size and similar fields if ZIP64 extensions are + * used. + */ + static final ZipLong ZIP64_MAGIC = new ZipLong(ZipConstants.ZIP64_MAGIC); + + /** + * Create instance from a number. + * + * @param value the long to store as a ZipLong + * @since 1.1 + */ + public ZipLong(long value) { + this.value = value; + } + + /** + * Create instance from bytes. + * + * @param bytes the bytes to store as a ZipLong + * @since 1.1 + */ + public ZipLong(byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the four bytes starting at offset. + * + * @param bytes the bytes to store as a ZipLong + * @param offset the offset to start + * @since 1.1 + */ + public ZipLong(byte[] bytes, int offset) { + value = ZipLong.getValue(bytes, offset); + } + + /** + * Get value as four bytes in big endian byte order. + * + * @since 1.1 + * @return value as four bytes in big endian order + */ + public byte[] getBytes() { + return ZipLong.getBytes(value); + } + + /** + * Get value as Java long. + * + * @since 1.1 + * @return value as a long + */ + public long getValue() { + return value; + } + + /** + * Get value as four bytes in big endian byte order. + * + * @param value the value to convert + * @return value as four bytes in big endian byte order + */ + public static byte[] getBytes(long value) { + byte[] result = new byte[WORD]; + result[0] = (byte) ((value & BYTE_MASK)); + result[BYTE_1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + result[BYTE_2] = (byte) ((value & BYTE_2_MASK) >> BYTE_2_SHIFT); + result[BYTE_3] = (byte) ((value & BYTE_3_MASK) >> BYTE_3_SHIFT); + return result; + } + + /** + * Helper method to get the value as a Java long from four bytes starting at given array offset + * + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding Java long value + */ + public static long getValue(byte[] bytes, int offset) { + long value = (bytes[offset + BYTE_3] << BYTE_3_SHIFT) & BYTE_3_MASK; + value += (bytes[offset + BYTE_2] << BYTE_2_SHIFT) & BYTE_2_MASK; + value += (bytes[offset + BYTE_1] << BYTE_1_SHIFT) & BYTE_1_MASK; + value += (bytes[offset] & BYTE_MASK); + return value; + } + + /** + * Helper method to get the value as a Java long from a four-byte array + * + * @param bytes the array of bytes + * @return the corresponding Java long value + */ + public static long getValue(byte[] bytes) { + return getValue(bytes, 0); + } + + /** + * Override to make two instances with same value equal. + * + * @param o an object to compare + * @return true if the objects are equal + * @since 1.1 + */ + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof ZipLong)) { + return false; + } + return value == ((ZipLong) o).getValue(); + } + + /** + * Override to make two instances with same value equal. + * + * @return the value stored in the ZipLong + * @since 1.1 + */ + @Override + public int hashCode() { + return (int) value; + } + + @Override + public Object clone() { + try { + return super.clone(); + } + catch (CloneNotSupportedException cnfe) { + // impossible + throw new RuntimeException(cnfe); + } + } + + @Override + public String toString() { + return "ZipLong value: " + value; + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/extra/ZipShort.java b/app/src/main/java/org/zeroturnaround/zip/extra/ZipShort.java new file mode 100644 index 0000000..d32770f --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/extra/ZipShort.java @@ -0,0 +1,169 @@ +/* + * 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.zeroturnaround.zip.extra; + +import static org.zeroturnaround.zip.extra.ZipConstants.BYTE_MASK; + +/** + * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally + * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package. + * All license and other documentation is intact. + * + * Utility class that represents a two byte integer with conversion + * rules for the big endian byte order of ZIP files. + * + */ +public final class ZipShort implements Cloneable { + private static final int BYTE_1_MASK = 0xFF00; + private static final int BYTE_1_SHIFT = 8; + + private final int value; + + /** + * Create instance from a number. + * + * @param value the int to store as a ZipShort + * @since 1.1 + */ + public ZipShort(int value) { + this.value = value; + } + + /** + * Create instance from bytes. + * + * @param bytes the bytes to store as a ZipShort + * @since 1.1 + */ + public ZipShort(byte[] bytes) { + this(bytes, 0); + } + + /** + * Create instance from the two bytes starting at offset. + * + * @param bytes the bytes to store as a ZipShort + * @param offset the offset to start + * @since 1.1 + */ + public ZipShort(byte[] bytes, int offset) { + value = ZipShort.getValue(bytes, offset); + } + + /** + * Get value as two bytes in big endian byte order. + * + * @return the value as a a two byte array in big endian byte order + * @since 1.1 + */ + public byte[] getBytes() { + byte[] result = new byte[2]; + result[0] = (byte) (value & BYTE_MASK); + result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + return result; + } + + /** + * Get value as Java int. + * + * @return value as a Java int + * @since 1.1 + */ + public int getValue() { + return value; + } + + /** + * Get value as two bytes in big endian byte order. + * + * @param value the Java int to convert to bytes + * @return the converted int as a byte array in big endian byte order + */ + public static byte[] getBytes(int value) { + byte[] result = new byte[2]; + result[0] = (byte) (value & BYTE_MASK); + result[1] = (byte) ((value & BYTE_1_MASK) >> BYTE_1_SHIFT); + return result; + } + + /** + * Helper method to get the value as a java int from two bytes starting at given array offset + * + * @param bytes the array of bytes + * @param offset the offset to start + * @return the corresponding java int value + */ + public static int getValue(byte[] bytes, int offset) { + int value = (bytes[offset + 1] << BYTE_1_SHIFT) & BYTE_1_MASK; + value += (bytes[offset] & BYTE_MASK); + return value; + } + + /** + * Helper method to get the value as a java int from a two-byte array + * + * @param bytes the array of bytes + * @return the corresponding java int value + */ + public static int getValue(byte[] bytes) { + return getValue(bytes, 0); + } + + /** + * Override to make two instances with same value equal. + * + * @param o an object to compare + * @return true if the objects are equal + * @since 1.1 + */ + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof ZipShort)) { + return false; + } + return value == ((ZipShort) o).getValue(); + } + + /** + * Override to make two instances with same value equal. + * + * @return the value stored in the ZipShort + * @since 1.1 + */ + @Override + public int hashCode() { + return value; + } + + @Override + public Object clone() { + try { + return super.clone(); + } + catch (CloneNotSupportedException cnfe) { + // impossible + throw new RuntimeException(cnfe); + } + } + + @Override + public String toString() { + return "ZipShort value: " + value; + } +} diff --git a/app/src/main/java/org/zeroturnaround/zip/transform/ByteArrayZipEntryTransformer.java b/app/src/main/java/org/zeroturnaround/zip/transform/ByteArrayZipEntryTransformer.java new file mode 100644 index 0000000..d7a5794 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/transform/ByteArrayZipEntryTransformer.java @@ -0,0 +1,64 @@ +package org.zeroturnaround.zip.transform; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.zeroturnaround.zip.ByteSource; +import org.zeroturnaround.zip.commons.IOUtils; + +public abstract class ByteArrayZipEntryTransformer implements ZipEntryTransformer { + + /** + * Transforms the given byte array into a new one. + * + * @param zipEntry + * entry to transform + * @param input + * entry contents + * + * @return byte[] + * the transformed contents of the entry + * + * @throws IOException + * if anything goes wrong + */ + protected abstract byte[] transform(ZipEntry zipEntry, byte[] input) throws IOException; + + /** + * Transforms the zip entry given as an input stream and ZipEntry metadata. + * The result is written to a ZipOutputStream + * * @param in input stream of the entry contents + * @param zipEntry zip entry metadata + * @param out output stream to write transformed entry + * + * @throws IOException if anything goes wrong + + */ + public void transform(InputStream in, ZipEntry zipEntry, ZipOutputStream out) throws IOException { + byte[] bytes = IOUtils.toByteArray(in); + bytes = transform(zipEntry, bytes); + + ByteSource source; + + if (preserveTimestamps()) { + source = new ByteSource(zipEntry.getName(), bytes, zipEntry.getTime()); + } + else { + source = new ByteSource(zipEntry.getName(), bytes); + } + + ZipEntrySourceZipEntryTransformer.addEntry(source, out); + } + + /** + * Override to return true if needed. + * + * @return true if this transformer should preserve timestamp of the entry it transforms, false otherwise + */ + protected boolean preserveTimestamps() { + return false; + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/transform/FileZipEntryTransformer.java b/app/src/main/java/org/zeroturnaround/zip/transform/FileZipEntryTransformer.java new file mode 100644 index 0000000..7164982 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/transform/FileZipEntryTransformer.java @@ -0,0 +1,73 @@ +package org.zeroturnaround.zip.transform; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.zeroturnaround.zip.FileSource; +import org.zeroturnaround.zip.commons.FileUtils; +import org.zeroturnaround.zip.commons.IOUtils; + +public abstract class FileZipEntryTransformer implements ZipEntryTransformer { + + /** + * Copies and transforms the given file into the output file. + * + * @param zipEntry + * zip entry metadata + * @param in + * zip entry contents + * @param out + * file to write transformed contents + * + * @throws IOException if file is not found or writing to it fails + * + */ + protected abstract void transform(ZipEntry zipEntry, File in, File out) throws IOException; + + /** + * Copies the input stream to the file, then transforms the file. + * FileSource is added then to the output stream. + * + * @param in + * input stream of the entry contents + * @param zipEntry + * zip entry metadata + * @param out + * ignored, because we're working on files + * + * @throws IOException if anything goes wrong + */ + public void transform(InputStream in, ZipEntry zipEntry, ZipOutputStream out) throws IOException { + File inFile = null; + File outFile = null; + try { + inFile = File.createTempFile("zip", null); + outFile = File.createTempFile("zip", null); + copy(in, inFile); + transform(zipEntry, inFile, outFile); + FileSource source = new FileSource(zipEntry.getName(), outFile); + ZipEntrySourceZipEntryTransformer.addEntry(source, out); + } + finally { + FileUtils.deleteQuietly(inFile); + FileUtils.deleteQuietly(outFile); + } + } + + private static void copy(InputStream in, File file) throws IOException { + OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); + try { + IOUtils.copy(in, out); + } + finally { + IOUtils.closeQuietly(out); + } + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/transform/StreamZipEntryTransformer.java b/app/src/main/java/org/zeroturnaround/zip/transform/StreamZipEntryTransformer.java new file mode 100644 index 0000000..85f0dad --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/transform/StreamZipEntryTransformer.java @@ -0,0 +1,44 @@ +package org.zeroturnaround.zip.transform; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + + +public abstract class StreamZipEntryTransformer implements ZipEntryTransformer { + + /** + * Copies and transforms the given input stream into the output stream. + * + * @param zipEntry + * zip entry metadata + * @param in + * zip entry contents + * @param out + * output stream to write the transformed entry + * + * @throws IOException if writing transformed entry fails + * + */ + protected abstract void transform(ZipEntry zipEntry, InputStream in, OutputStream out) throws IOException; + + /** + * Transforms the input stream entry, writes that to output stream, closes entry in the output stream. + * + * @param in input stream of the entry contents + * @param zipEntry zip entry metadata + * @param out output stream to write transformed entry (if necessary) + * + * @throws IOException if anything goes wrong + */ + public void transform(InputStream in, ZipEntry zipEntry, ZipOutputStream out) throws IOException { + ZipEntry entry = new ZipEntry(zipEntry.getName()); + entry.setTime(System.currentTimeMillis()); + out.putNextEntry(entry); + transform(zipEntry, in, out); + out.closeEntry(); + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/transform/StringZipEntryTransformer.java b/app/src/main/java/org/zeroturnaround/zip/transform/StringZipEntryTransformer.java new file mode 100644 index 0000000..e2d2eec --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/transform/StringZipEntryTransformer.java @@ -0,0 +1,48 @@ +package org.zeroturnaround.zip.transform; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.zeroturnaround.zip.ByteSource; +import org.zeroturnaround.zip.commons.IOUtils; + +public abstract class StringZipEntryTransformer implements ZipEntryTransformer { + + /** + * The encoding to use, null means platform default. + */ + private final String encoding; + + public StringZipEntryTransformer() { + this(null); + } + + public StringZipEntryTransformer(String encoding) { + this.encoding = encoding; + } + + /** + * Transforms the given String into a new one. + * + * @param zipEntry + * zip entry metadata + * @param input + * zip entry contents + * + * @return String - transformed entry contents + * + * @throws IOException if transformation cannot be completed succesfully + */ + protected abstract String transform(ZipEntry zipEntry, String input) throws IOException; + + public void transform(InputStream in, ZipEntry zipEntry, ZipOutputStream out) throws IOException { + String data = IOUtils.toString(in, encoding); + data = transform(zipEntry, data); + byte[] bytes = encoding == null ? data.getBytes() : data.getBytes(encoding); + ByteSource source = new ByteSource(zipEntry.getName(), bytes); + ZipEntrySourceZipEntryTransformer.addEntry(source, out); + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntrySourceZipEntryTransformer.java b/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntrySourceZipEntryTransformer.java new file mode 100644 index 0000000..69d4da5 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntrySourceZipEntryTransformer.java @@ -0,0 +1,45 @@ +package org.zeroturnaround.zip.transform; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.zeroturnaround.zip.ZipEntrySource; +import org.zeroturnaround.zip.commons.IOUtils; + +public class ZipEntrySourceZipEntryTransformer implements ZipEntryTransformer { + + private final ZipEntrySource source; + + public ZipEntrySourceZipEntryTransformer(ZipEntrySource source) { + this.source = source; + } + + public void transform(InputStream in, ZipEntry zipEntry, ZipOutputStream out) throws IOException { + addEntry(source, out); + } + + /** + * Adds a given ZIP entry to a ZIP file. + * + * @param entry + * new ZIP entry. + * @param out + * target ZIP stream. + */ + static void addEntry(ZipEntrySource entry, ZipOutputStream out) throws IOException { + out.putNextEntry(entry.getEntry()); + InputStream in = entry.getInputStream(); + if (in != null) { + try { + IOUtils.copy(in, out); + } + finally { + IOUtils.closeQuietly(in); + } + } + out.closeEntry(); + } + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntryTransformer.java b/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntryTransformer.java new file mode 100644 index 0000000..729f083 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntryTransformer.java @@ -0,0 +1,27 @@ +package org.zeroturnaround.zip.transform; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * Call-back for traversing ZIP entries with their contents and producing a new ZIP file as an output. + * + * @author Rein Raudjärv + */ +public interface ZipEntryTransformer { + + /** + * Transforms the zip entry given as an input stream and ZipEntry metadata. + * The result is written to a ZipOutputStream + * + * @param in input stream of the entry contents + * @param zipEntry zip entry metadata + * @param out output stream to write transformed entry (if necessary) + * + * @throws IOException if anything goes wrong + */ + void transform(InputStream in, ZipEntry zipEntry, ZipOutputStream out) throws IOException; + +} diff --git a/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntryTransformerEntry.java b/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntryTransformerEntry.java new file mode 100644 index 0000000..f10b441 --- /dev/null +++ b/app/src/main/java/org/zeroturnaround/zip/transform/ZipEntryTransformerEntry.java @@ -0,0 +1,31 @@ +package org.zeroturnaround.zip.transform; + +/** + * A transformer assigned to a certain ZIP entry. + * + * @author Rein Raudjärv + */ +public class ZipEntryTransformerEntry { + + private final String path; + + private final ZipEntryTransformer transformer; + + public ZipEntryTransformerEntry(String path, ZipEntryTransformer transformer) { + this.path = path; + this.transformer = transformer; + } + + public String getPath() { + return path; + } + + public ZipEntryTransformer getTransformer() { + return transformer; + } + + public String toString() { + return path + "=" + transformer; + } + +} diff --git a/build.gradle b/build.gradle index a5cf75f..7a1f2e8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:1.2.3' } }