mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-04-18 21:01:23 +00:00
Merge pull request from GHSA-wxrm-jhpf-vp6v
Fix preferences import vulnerability
This commit is contained in:
@@ -6,6 +6,7 @@ package org.schabi.newpipe.error;
|
||||
public enum UserAction {
|
||||
USER_REPORT("user report"),
|
||||
UI_ERROR("ui error"),
|
||||
DATABASE_IMPORT_EXPORT("database import or export"),
|
||||
SUBSCRIPTION_CHANGE("subscription change"),
|
||||
SUBSCRIPTION_UPDATE("subscription update"),
|
||||
SUBSCRIPTION_GET("get subscription"),
|
||||
|
||||
@@ -21,9 +21,15 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.settings.export.BackupFileLocator;
|
||||
import org.schabi.newpipe.settings.export.ImportExportManager;
|
||||
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -42,7 +48,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
private final SimpleDateFormat exportDateFormat =
|
||||
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
||||
private ContentSettingsManager manager;
|
||||
private ImportExportManager manager;
|
||||
private String importExportDataPathKey;
|
||||
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
||||
@@ -57,8 +63,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
@Nullable final String rootKey) {
|
||||
final File homeDir = ContextCompat.getDataDir(requireContext());
|
||||
Objects.requireNonNull(homeDir);
|
||||
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
|
||||
manager.deleteSettingsFile();
|
||||
manager = new ImportExportManager(new BackupFileLocator(homeDir));
|
||||
|
||||
importExportDataPathKey = getString(R.string.import_export_data_path);
|
||||
|
||||
@@ -165,7 +170,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e);
|
||||
showErrorSnackbar(e, "Exporting database and settings");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +187,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
throw new IOException("Could not create databases dir");
|
||||
}
|
||||
|
||||
// replace the current database
|
||||
if (!manager.extractDb(file)) {
|
||||
Toast.makeText(requireContext(), R.string.could_not_import_all_files,
|
||||
Toast.LENGTH_LONG)
|
||||
@@ -189,9 +195,13 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
|
||||
// if settings file exist, ask if it should be imported.
|
||||
if (manager.extractSettings(file)) {
|
||||
final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file);
|
||||
if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) {
|
||||
new androidx.appcompat.app.AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.import_settings)
|
||||
.setMessage(hasJsonPrefs ? null : requireContext()
|
||||
.getString(R.string.import_settings_vulnerable_format))
|
||||
.setOnDismissListener(dialog -> finishImport(importDataUri))
|
||||
.setNegativeButton(R.string.cancel, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
finishImport(importDataUri);
|
||||
@@ -201,7 +211,16 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
final Context context = requireContext();
|
||||
final SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
manager.loadSharedPreferences(prefs);
|
||||
try {
|
||||
if (hasJsonPrefs) {
|
||||
manager.loadJsonPrefs(file, prefs);
|
||||
} else {
|
||||
manager.loadSerializedPrefs(file, prefs);
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException | JsonParserException e) {
|
||||
createErrorNotification(e, "Importing preferences");
|
||||
return;
|
||||
}
|
||||
cleanImport(context, prefs);
|
||||
finishImport(importDataUri);
|
||||
})
|
||||
@@ -210,7 +229,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
finishImport(importDataUri);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Importing database", e);
|
||||
showErrorSnackbar(e, "Importing database and settings");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +266,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save import path and restart system.
|
||||
* Save import path and restart app.
|
||||
*
|
||||
* @param importDataUri The import path to save
|
||||
*/
|
||||
@@ -268,4 +287,15 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
|
||||
.putString(importExportDataPathKey, importExportDataUri.toString());
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private void showErrorSnackbar(final Throwable e, final String request) {
|
||||
ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request));
|
||||
}
|
||||
|
||||
private void createErrorNotification(final Throwable e, final String request) {
|
||||
ErrorUtil.createNotification(
|
||||
requireContext(),
|
||||
new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package org.schabi.newpipe.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import org.schabi.newpipe.MainActivity.DEBUG
|
||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
|
||||
companion object {
|
||||
const val TAG = "ContentSetManager"
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports given [SharedPreferences] to the file in given outputPath.
|
||||
* It also creates the file.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
|
||||
file.create()
|
||||
ZipOutputStream(SharpOutputStream(file.stream).buffered())
|
||||
.use { outZip ->
|
||||
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
|
||||
|
||||
try {
|
||||
ObjectOutputStream(fileLocator.settings.outputStream()).use { output ->
|
||||
output.writeObject(preferences.all)
|
||||
output.flush()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Unable to exportDatabase", e)
|
||||
}
|
||||
}
|
||||
|
||||
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSettingsFile() {
|
||||
fileLocator.settings.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to create database directory if it does not exist.
|
||||
*
|
||||
* @return Whether the directory exists afterwards.
|
||||
*/
|
||||
fun ensureDbDirectoryExists(): Boolean {
|
||||
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
|
||||
}
|
||||
|
||||
fun extractDb(file: StoredFileHelper): Boolean {
|
||||
val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
|
||||
if (success) {
|
||||
fileLocator.dbJournal.delete()
|
||||
fileLocator.dbWal.delete()
|
||||
fileLocator.dbShm.delete()
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
fun extractSettings(file: StoredFileHelper): Boolean {
|
||||
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all shared preferences from the app and load the preferences supplied to the manager.
|
||||
*/
|
||||
fun loadSharedPreferences(preferences: SharedPreferences) {
|
||||
try {
|
||||
val preferenceEditor = preferences.edit()
|
||||
|
||||
ObjectInputStream(fileLocator.settings.inputStream()).use { input ->
|
||||
preferenceEditor.clear()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val entries = input.readObject() as Map<String, *>
|
||||
for ((key, value) in entries) {
|
||||
when (value) {
|
||||
is Boolean -> {
|
||||
preferenceEditor.putBoolean(key, value)
|
||||
}
|
||||
is Float -> {
|
||||
preferenceEditor.putFloat(key, value)
|
||||
}
|
||||
is Int -> {
|
||||
preferenceEditor.putInt(key, value)
|
||||
}
|
||||
is Long -> {
|
||||
preferenceEditor.putLong(key, value)
|
||||
}
|
||||
is String -> {
|
||||
preferenceEditor.putString(key, value)
|
||||
}
|
||||
is Set<*> -> {
|
||||
// There are currently only Sets with type String possible
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferenceEditor.putStringSet(key, value as Set<String>?)
|
||||
}
|
||||
}
|
||||
}
|
||||
preferenceEditor.commit()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "Unable to loadSharedPreferences", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.schabi.newpipe.settings
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Locates specific files of NewPipe based on the home directory of the app.
|
||||
*/
|
||||
class NewPipeFileLocator(private val homeDir: File) {
|
||||
|
||||
val dbDir by lazy { File(homeDir, "/databases") }
|
||||
|
||||
val db by lazy { File(homeDir, "/databases/newpipe.db") }
|
||||
|
||||
val dbJournal by lazy { File(homeDir, "/databases/newpipe.db-journal") }
|
||||
|
||||
val dbShm by lazy { File(homeDir, "/databases/newpipe.db-shm") }
|
||||
|
||||
val dbWal by lazy { File(homeDir, "/databases/newpipe.db-wal") }
|
||||
|
||||
val settings by lazy { File(homeDir, "/databases/newpipe.settings") }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.schabi.newpipe.settings.export
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Locates specific files of NewPipe based on the home directory of the app.
|
||||
*/
|
||||
class BackupFileLocator(private val homeDir: File) {
|
||||
companion object {
|
||||
const val FILE_NAME_DB = "newpipe.db"
|
||||
@Deprecated(
|
||||
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
|
||||
replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS")
|
||||
)
|
||||
const val FILE_NAME_SERIALIZED_PREFS = "newpipe.settings"
|
||||
const val FILE_NAME_JSON_PREFS = "preferences.json"
|
||||
}
|
||||
|
||||
val dbDir by lazy { File(homeDir, "/databases") }
|
||||
|
||||
val db by lazy { File(dbDir, FILE_NAME_DB) }
|
||||
|
||||
val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") }
|
||||
|
||||
val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") }
|
||||
|
||||
val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") }
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.schabi.newpipe.settings.export
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.grack.nanojson.JsonArray
|
||||
import com.grack.nanojson.JsonParser
|
||||
import com.grack.nanojson.JsonParserException
|
||||
import com.grack.nanojson.JsonWriter
|
||||
import org.schabi.newpipe.streams.io.SharpOutputStream
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper
|
||||
import org.schabi.newpipe.util.ZipHelper
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.ObjectOutputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class ImportExportManager(private val fileLocator: BackupFileLocator) {
|
||||
companion object {
|
||||
const val TAG = "ImportExportManager"
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports given [SharedPreferences] to the file in given outputPath.
|
||||
* It also creates the file.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
|
||||
file.create()
|
||||
ZipOutputStream(SharpOutputStream(file.stream).buffered()).use { outZip ->
|
||||
// add the database
|
||||
ZipHelper.addFileToZip(
|
||||
outZip,
|
||||
BackupFileLocator.FILE_NAME_DB,
|
||||
fileLocator.db.path,
|
||||
)
|
||||
|
||||
// add the legacy vulnerable serialized preferences (will be removed in the future)
|
||||
ZipHelper.addFileToZip(
|
||||
outZip,
|
||||
BackupFileLocator.FILE_NAME_SERIALIZED_PREFS
|
||||
) { byteOutput ->
|
||||
ObjectOutputStream(byteOutput).use { output ->
|
||||
output.writeObject(preferences.all)
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
|
||||
// add the JSON preferences
|
||||
ZipHelper.addFileToZip(
|
||||
outZip,
|
||||
BackupFileLocator.FILE_NAME_JSON_PREFS
|
||||
) { byteOutput ->
|
||||
JsonWriter
|
||||
.indent("")
|
||||
.on(byteOutput)
|
||||
.`object`(preferences.all)
|
||||
.done()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to create database directory if it does not exist.
|
||||
*
|
||||
* @return Whether the directory exists afterwards.
|
||||
*/
|
||||
fun ensureDbDirectoryExists(): Boolean {
|
||||
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the database from the given file to the app's database directory.
|
||||
* The current app's database will be overwritten.
|
||||
* @param file the .zip file to extract the database from
|
||||
* @return true if the database was successfully extracted, false otherwise
|
||||
*/
|
||||
fun extractDb(file: StoredFileHelper): Boolean {
|
||||
val success = ZipHelper.extractFileFromZip(
|
||||
file,
|
||||
BackupFileLocator.FILE_NAME_DB,
|
||||
fileLocator.db.path,
|
||||
)
|
||||
|
||||
if (success) {
|
||||
fileLocator.dbJournal.delete()
|
||||
fileLocator.dbWal.delete()
|
||||
fileLocator.dbShm.delete()
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
|
||||
replaceWith = ReplaceWith("exportHasJsonPrefs")
|
||||
)
|
||||
fun exportHasSerializedPrefs(zipFile: StoredFileHelper): Boolean {
|
||||
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS)
|
||||
}
|
||||
|
||||
fun exportHasJsonPrefs(zipFile: StoredFileHelper): Boolean {
|
||||
return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all shared preferences from the app and load the preferences supplied to the manager.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
|
||||
replaceWith = ReplaceWith("loadJsonPrefs")
|
||||
)
|
||||
@Throws(IOException::class, ClassNotFoundException::class)
|
||||
fun loadSerializedPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
|
||||
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) {
|
||||
PreferencesObjectInputStream(it).use { input ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val entries = input.readObject() as Map<String, *>
|
||||
|
||||
val editor = preferences.edit()
|
||||
editor.clear()
|
||||
|
||||
for ((key, value) in entries) {
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(key, value)
|
||||
is Float -> editor.putFloat(key, value)
|
||||
is Int -> editor.putInt(key, value)
|
||||
is Long -> editor.putLong(key, value)
|
||||
is String -> editor.putString(key, value)
|
||||
is Set<*> -> {
|
||||
// There are currently only Sets with type String possible
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
editor.putStringSet(key, value as Set<String>?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!editor.commit()) {
|
||||
throw IOException("Unable to commit loadSerializedPrefs")
|
||||
}
|
||||
}
|
||||
}.let { fileExists ->
|
||||
if (!fileExists) {
|
||||
throw FileNotFoundException(BackupFileLocator.FILE_NAME_SERIALIZED_PREFS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all shared preferences from the app and load the preferences supplied to the manager.
|
||||
*/
|
||||
@Throws(IOException::class, JsonParserException::class)
|
||||
fun loadJsonPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
|
||||
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) {
|
||||
val jsonObject = JsonParser.`object`().from(it)
|
||||
|
||||
val editor = preferences.edit()
|
||||
editor.clear()
|
||||
|
||||
for ((key, value) in jsonObject) {
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(key, value)
|
||||
is Float -> editor.putFloat(key, value)
|
||||
is Int -> editor.putInt(key, value)
|
||||
is Long -> editor.putLong(key, value)
|
||||
is String -> editor.putString(key, value)
|
||||
is JsonArray -> {
|
||||
editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!editor.commit()) {
|
||||
throw IOException("Unable to commit loadJsonPrefs")
|
||||
}
|
||||
}.let { fileExists ->
|
||||
if (!fileExists) {
|
||||
throw FileNotFoundException(BackupFileLocator.FILE_NAME_JSON_PREFS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.schabi.newpipe.settings.export;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectStreamClass;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
|
||||
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
|
||||
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
|
||||
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
|
||||
* cmu.edu
|
||||
* </a>,
|
||||
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
|
||||
* OWASP cheatsheet
|
||||
* </a>,
|
||||
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
|
||||
* Apache's {@code ValidatingObjectInputStream}
|
||||
* </a>
|
||||
*/
|
||||
public class PreferencesObjectInputStream extends ObjectInputStream {
|
||||
|
||||
/**
|
||||
* Primitive types, strings and other built-in types do not pass through resolveClass() but
|
||||
* instead have a custom encoding; see
|
||||
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
|
||||
* official docs</a>.
|
||||
*/
|
||||
private static final Set<String> CLASS_WHITELIST = Set.of(
|
||||
"java.lang.Boolean",
|
||||
"java.lang.Byte",
|
||||
"java.lang.Character",
|
||||
"java.lang.Short",
|
||||
"java.lang.Integer",
|
||||
"java.lang.Long",
|
||||
"java.lang.Float",
|
||||
"java.lang.Double",
|
||||
"java.lang.Void",
|
||||
"java.util.HashMap",
|
||||
"java.util.HashSet"
|
||||
);
|
||||
|
||||
public PreferencesObjectInputStream(final InputStream in) throws IOException {
|
||||
super(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> resolveClass(final ObjectStreamClass desc)
|
||||
throws ClassNotFoundException, IOException {
|
||||
if (CLASS_WHITELIST.contains(desc.getName())) {
|
||||
return super.resolveClass(desc);
|
||||
} else {
|
||||
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package org.schabi.newpipe.util;
|
||||
|
||||
import org.schabi.newpipe.streams.io.SharpInputStream;
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
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.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 28.01.18.
|
||||
* Copyright 2018 Christian Schabesberger <chris.schabesberger@mailbox.org>
|
||||
@@ -34,73 +37,154 @@ import org.schabi.newpipe.streams.io.StoredFileHelper;
|
||||
*/
|
||||
|
||||
public final class ZipHelper {
|
||||
private ZipHelper() { }
|
||||
|
||||
private static final int BUFFER_SIZE = 2048;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface InputStreamConsumer {
|
||||
void acceptStream(InputStream inputStream) throws IOException;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OutputStreamConsumer {
|
||||
void acceptStream(OutputStream outputStream) throws IOException;
|
||||
}
|
||||
|
||||
|
||||
private ZipHelper() { }
|
||||
|
||||
|
||||
/**
|
||||
* This function helps to create zip files.
|
||||
* Caution this will override the original file.
|
||||
* This function helps to create zip files. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param outZip The ZipOutputStream where the data should be stored in
|
||||
* @param file The path of the file that should be added to zip.
|
||||
* @param name The path of the file inside the zip.
|
||||
* @throws Exception
|
||||
* @param outZip the ZipOutputStream where the data should be stored in
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param fileOnDisk the path of the file on the disk that should be added to zip
|
||||
*/
|
||||
public static void addFileToZip(final ZipOutputStream outZip, final String file,
|
||||
final String name) throws Exception {
|
||||
public static void addFileToZip(final ZipOutputStream outZip,
|
||||
final String nameInZip,
|
||||
final String fileOnDisk) throws IOException {
|
||||
try (FileInputStream fi = new FileInputStream(fileOnDisk)) {
|
||||
addFileToZip(outZip, nameInZip, fi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function helps to create zip files. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param outZip the ZipOutputStream where the data should be stored in
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param streamConsumer will be called with an output stream that will go to the output file
|
||||
*/
|
||||
public static void addFileToZip(final ZipOutputStream outZip,
|
||||
final String nameInZip,
|
||||
final OutputStreamConsumer streamConsumer) throws IOException {
|
||||
final byte[] bytes;
|
||||
try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) {
|
||||
streamConsumer.acceptStream(byteOutput);
|
||||
bytes = byteOutput.toByteArray();
|
||||
}
|
||||
|
||||
try (ByteArrayInputStream byteInput = new ByteArrayInputStream(bytes)) {
|
||||
ZipHelper.addFileToZip(outZip, nameInZip, byteInput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function helps to create zip files. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param outZip the ZipOutputStream where the data should be stored in
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param inputStream the content to put inside the file
|
||||
*/
|
||||
public static void addFileToZip(final ZipOutputStream outZip,
|
||||
final String nameInZip,
|
||||
final InputStream inputStream) throws IOException {
|
||||
final byte[] data = new byte[BUFFER_SIZE];
|
||||
try (FileInputStream fi = new FileInputStream(file);
|
||||
BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE)) {
|
||||
final ZipEntry entry = new ZipEntry(name);
|
||||
try (BufferedInputStream bufferedInputStream =
|
||||
new BufferedInputStream(inputStream, BUFFER_SIZE)) {
|
||||
final ZipEntry entry = new ZipEntry(nameInZip);
|
||||
outZip.putNextEntry(entry);
|
||||
int count;
|
||||
while ((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) {
|
||||
while ((count = bufferedInputStream.read(data, 0, BUFFER_SIZE)) != -1) {
|
||||
outZip.write(data, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will extract data from ZipInputStream.
|
||||
* Caution this will override the original file.
|
||||
* This will extract data from ZipInputStream. Caution this will overwrite the original file.
|
||||
*
|
||||
* @param zipFile The zip file
|
||||
* @param file The path of the file on the disk where the data should be extracted to.
|
||||
* @param name The path of the file inside the zip.
|
||||
* @param zipFile the zip file to extract from
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param fileOnDisk the path of the file on the disk where the data should be extracted to
|
||||
* @return will return true if the file was found within the zip file
|
||||
* @throws Exception
|
||||
*/
|
||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile, final String file,
|
||||
final String name) throws Exception {
|
||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
|
||||
final String nameInZip,
|
||||
final String fileOnDisk) throws IOException {
|
||||
return extractFileFromZip(zipFile, nameInZip, input -> {
|
||||
// delete old file first
|
||||
final File oldFile = new File(fileOnDisk);
|
||||
if (oldFile.exists()) {
|
||||
if (!oldFile.delete()) {
|
||||
throw new IOException("Could not delete " + fileOnDisk);
|
||||
}
|
||||
}
|
||||
|
||||
final byte[] data = new byte[BUFFER_SIZE];
|
||||
try (FileOutputStream outFile = new FileOutputStream(fileOnDisk)) {
|
||||
int count;
|
||||
while ((count = input.read(data)) != -1) {
|
||||
outFile.write(data, 0, count);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This will extract data from ZipInputStream.
|
||||
*
|
||||
* @param zipFile the zip file to extract from
|
||||
* @param nameInZip the path of the file inside the zip
|
||||
* @param streamConsumer will be called with the input stream from the file inside the zip
|
||||
* @return will return true if the file was found within the zip file
|
||||
*/
|
||||
public static boolean extractFileFromZip(final StoredFileHelper zipFile,
|
||||
final String nameInZip,
|
||||
final InputStreamConsumer streamConsumer)
|
||||
throws IOException {
|
||||
try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream(
|
||||
new SharpInputStream(zipFile.getStream())))) {
|
||||
ZipEntry ze;
|
||||
while ((ze = inZip.getNextEntry()) != null) {
|
||||
if (ze.getName().equals(nameInZip)) {
|
||||
streamConsumer.acceptStream(inZip);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param zipFile the zip file
|
||||
* @param fileInZip the filename to check
|
||||
* @return whether the provided filename is in the zip; only the first level is checked
|
||||
*/
|
||||
public static boolean zipContainsFile(final StoredFileHelper zipFile, final String fileInZip)
|
||||
throws Exception {
|
||||
try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream(
|
||||
new SharpInputStream(zipFile.getStream())))) {
|
||||
final byte[] data = new byte[BUFFER_SIZE];
|
||||
boolean found = false;
|
||||
ZipEntry ze;
|
||||
|
||||
while ((ze = inZip.getNextEntry()) != null) {
|
||||
if (ze.getName().equals(name)) {
|
||||
found = true;
|
||||
// delete old file first
|
||||
final File oldFile = new File(file);
|
||||
if (oldFile.exists()) {
|
||||
if (!oldFile.delete()) {
|
||||
throw new Exception("Could not delete " + file);
|
||||
}
|
||||
}
|
||||
|
||||
try (FileOutputStream outFile = new FileOutputStream(file)) {
|
||||
int count = 0;
|
||||
while ((count = inZip.read(data)) != -1) {
|
||||
outFile.write(data, 0, count);
|
||||
}
|
||||
}
|
||||
|
||||
inZip.closeEntry();
|
||||
if (ze.getName().equals(fileInZip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user