diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/Main.java b/src/main/java/com/hrudyplayz/mcinstanceloader/Main.java index 7fa9291..3b3783d 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/Main.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/Main.java @@ -82,7 +82,7 @@ public static void secondPhase() { // ===== STEP 4: Zip the pack folder ===== if (!Config.disableAutomaticZipCreation && !FileHelper.exists(Config.configFolder + "pack.mcinstance") && !FileHelper.exists(Config.configFolder + "pack.mcinstance.disabled")) { LogHelper.info("No pack.instance file found, created one from the pack folder."); - ZipHelper.zip(Config.configFolder + "pack", Config.configFolder + "pack.mcinstance"); + ZipHelper.zip(Config.configFolder + "pack", Config.configFolder + "pack.mcinstance", false, true); } // ===== STEP 5: Resources download ===== @@ -432,11 +432,11 @@ public static void downloadResources() { if (object.downloadFile()) { if (!object.checkHash()) throwError("Could not verify the hash of " + object.name + "."); else if (!object.checkCache() && !Config.disableCache) { - if (object.SHA512 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA512-" + object.SHA512, true); - else if (object.SHA256 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA256-" + object.SHA256, true); - else if (object.SHA1 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA1-" + object.SHA1, true); - else if (object.MD5 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "MD5-" + object.MD5, true); - else if (object.CRC32 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "CRC32-" + object.CRC32, true); + if (object.SHA512 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA512-" + object.SHA512); + else if (object.SHA256 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA256-" + object.SHA256); + else if (object.SHA1 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA1-" + object.SHA1); + else if (object.MD5 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "MD5-" + object.MD5); + else if (object.CRC32 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "CRC32-" + object.CRC32); } } else throwError("Error while downloading " + object.name + "."); @@ -473,7 +473,7 @@ public static void copyOverrides() { LogHelper.verboseInfo("Merging " + s + " with the original folder."); // Tries to move (merge) the file, if it fails it throws an error. - if (!FileHelper.copy(Config.configFolder + "temp" + File.separator + "overrides" + File.separator + s, s, false)) throwError("Error while merging the file " + s + " from the overrides folder."); + if (!FileHelper.copy(Config.configFolder + "temp" + File.separator + "overrides" + File.separator + s, s)) throwError("Error while merging the file " + s + " from the overrides folder."); errorContext = ""; // Resets the errorContext, so it can be reused for the next resource or the next step. } @@ -505,7 +505,7 @@ public static void copyCarryover() { LogHelper.verboseInfo("Merging " + s + " from carryover to the root folder."); // Tries to copy (merge) the file, if it fails it throws an error. - if (!FileHelper.copy("carryover" + File.separator + s, s, false)) throwError("Error while merging the file " + s + " from the carryover folder."); + if (!FileHelper.copy("carryover" + File.separator + s, s)) throwError("Error while merging the file " + s + " from the carryover folder."); errorContext = ""; // Resets the errorContext, so it can be reused for the next resource or the next step. } @@ -530,7 +530,7 @@ public static void finalSetup() { path = Config.configFolder + "pack.mcinstance"; if (!Config.skipFileDisabling && FileHelper.exists(path)) { // If the config to skip the file disabling wasn't set, it will disable or remove it. if (Config.deleteInsteadOfRenaming) FileHelper.delete(path); - else FileHelper.move(path, path + ".disabled", true); + else FileHelper.move(path, path + ".disabled"); } } @@ -540,7 +540,7 @@ public static void finalSetup() { } - public static void throwError (String text) { + public static void throwError(String text) { // Sets the final results screen to be an error screen. Adds an error any time it gets called. // If the amount of errors displayed is above the maximum limit, it adds to the more counter instead. @@ -570,7 +570,7 @@ public static void throwError (String text) { } } - public static void throwSuccess (String text) { + public static void throwSuccess(String text) { // Sets the final results screen to be the success screen, and adds the success message in it. if (side.equals("server")) { @@ -591,7 +591,7 @@ public static void throwSuccess (String text) { } - public static void throwUpdateScreen () { + public static void throwUpdateScreen() { // Sets the final results screen to be the mod update screen. if (side.equals("server")) return; diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/ModProperties.java b/src/main/java/com/hrudyplayz/mcinstanceloader/ModProperties.java index 076499c..0556a2a 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/ModProperties.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/ModProperties.java @@ -10,7 +10,7 @@ public class ModProperties { // General values public static final String MODID = "mcinstanceloader"; public static final String NAME = "MCInstance Loader"; - public static final String VERSION = "2.1"; + public static final String VERSION = "2.2"; public static final String MC_VERSION = "1.7.10"; public static final String URL = "https://github.com/HRudyPlayZ/MCInstanceLoader/"; @@ -48,7 +48,7 @@ public class ModProperties { "Also try ResourcceLoader", "Finally updated!", "Check the documentation on Github.", - "2.0.0.0.0, also known as 2.0: The overhaul!", + "2.2, finally bug-free (i hope).", "Since you're here, you might want to support the mod on Github and Modrinth!", "The most memes you can get, in one package.", "Every modpack needs this!", diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/gui/InfoGui.java b/src/main/java/com/hrudyplayz/mcinstanceloader/gui/InfoGui.java index 2b4c14b..ac51d41 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/gui/InfoGui.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/gui/InfoGui.java @@ -206,7 +206,7 @@ public void drawScreen(int x, int y, float renderPartialTicks) { } - public GuiButton createButton (int id, int x, int y, int width, String text) { + public GuiButton createButton(int id, int x, int y, int width, String text) { // Helper function to create buttons more easily, and return them to modify them afterwards. GuiButton result = new GuiButton(id, x, y, width, 20, text); diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/gui/OptionalModsGui.java b/src/main/java/com/hrudyplayz/mcinstanceloader/gui/OptionalModsGui.java index 6fb972b..1b04b45 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/gui/OptionalModsGui.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/gui/OptionalModsGui.java @@ -58,6 +58,7 @@ public class OptionalModsGui extends GuiScreen { public static boolean runOnceDone = false; // Only runs the runOnce function once. public static boolean cantClick = false; // Prevents double clicking on buttons. + public static boolean waitingText = false; // Whether the confirm button text should show the waiting text instead. public GuiScreen parentGuiScreen; @@ -143,6 +144,7 @@ private void refreshGui() { if (pageList.size() == 0) { currentMenu += 1; currentPage = 0; + waitingText = false; updateChecked(); refreshGui(); return; @@ -202,7 +204,10 @@ private void refreshGui() { // Adds the "Confirm and continue" button, with the appropriate color. String color = ""; if (!(checkedOptions.size() >= optionalList[currentMenu].minimumCheckedAmount)) color = "" + EnumChatFormatting.GRAY; - createButton(14, nextButtonX, buttonHeight, buttonWidth, color + I18n.format("gui.mcinstanceloader.confirm")); + + String text = I18n.format("gui.mcinstanceloader.confirm"); + if (waitingText) text = I18n.format("gui.mcinstanceloader.waitbutton"); + createButton(14, nextButtonX, buttonHeight, buttonWidth, color + text); } @Override @@ -286,10 +291,14 @@ protected void actionPerformed(GuiButton button) { } if (button.id == 14 && checkedOptions.size() >= optionalList[currentMenu].minimumCheckedAmount) { + waitingText = true; + refreshGui(); + for (OptionalResourcesHandler.MenuOption o : checkedOptions) o.download(); currentMenu += 1; currentPage = 0; + waitingText = false; updateChecked(); refreshGui(); } @@ -335,7 +344,7 @@ public void drawScreen(int x, int y, float renderPartialTicks) { } - public GuiButton createButton (int id, int x, int y, int width, String text) { + public GuiButton createButton(int id, int x, int y, int width, String text) { // Helper function to create buttons more easily, and return them to modify them afterwards. GuiButton result = new GuiButton(id, x, y, width, 20, text); diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/resources/OptionalResourcesHandler.java b/src/main/java/com/hrudyplayz/mcinstanceloader/resources/OptionalResourcesHandler.java index c1d4774..08c2567 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/resources/OptionalResourcesHandler.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/resources/OptionalResourcesHandler.java @@ -57,11 +57,11 @@ public void download() { if (object.downloadFile()) { if (!object.checkHash()) Main.throwError("Could not verify the hash of " + object.name + "."); else if (!object.checkCache() && !Config.disableCache) { - if (object.SHA512 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA512-" + object.SHA512, true); - else if (object.SHA256 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA256-" + object.SHA256, true); - else if (object.SHA1 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA1-" + object.SHA1, true); - else if (object.MD5 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "MD5-" + object.MD5, true); - else if (object.CRC32 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "CRC32-" + object.CRC32, true); + if (object.SHA512 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA512-" + object.SHA512); + else if (object.SHA256 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA256-" + object.SHA256); + else if (object.SHA1 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "SHA1-" + object.SHA1); + else if (object.MD5 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "MD5-" + object.MD5); + else if (object.CRC32 != null) FileHelper.copy(object.destination, "mcinstance-cache" + File.separator + "CRC32-" + object.CRC32); } } else Main.throwError("Error while downloading " + object.name + "."); diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/resources/ResourceObject.java b/src/main/java/com/hrudyplayz/mcinstanceloader/resources/ResourceObject.java index d4d9b7b..2a83a70 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/resources/ResourceObject.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/resources/ResourceObject.java @@ -1,8 +1,10 @@ package com.hrudyplayz.mcinstanceloader.resources; +import net.minecraft.client.Minecraft; import java.io.File; import java.io.IOException; import java.net.URLEncoder; +import java.util.concurrent.TimeUnit; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; @@ -35,6 +37,8 @@ public class ResourceObject { public String destination = ""; public String side = "both"; + private boolean hasTriedHashChecks; + public String SHA512; public String SHA256; public String SHA1; @@ -80,8 +84,22 @@ public boolean checkHash() { } catch (IOException e) { - Main.errorContext = "Error while checking the SHA-512 hash."; - return false; + if (!this.hasTriedHashChecks) { + this.hasTriedHashChecks = true; + LogHelper.info("An error occured while checking the SHA-512 hash, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + + return this.checkHash(); + } + else { + e.printStackTrace(); + Main.errorContext = "System error while checking the SHA-512 hash."; + + return false; + } } } @@ -96,8 +114,22 @@ public boolean checkHash() { } catch (IOException e) { - Main.errorContext = "Error while checking the SHA-256 hash."; - return false; + if (!this.hasTriedHashChecks) { + this.hasTriedHashChecks = true; + LogHelper.info("An error occured while checking the SHA-256 hash, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + + return this.checkHash(); + } + else { + e.printStackTrace(); + Main.errorContext = "System error while checking the SHA-256 hash."; + + return false; + } } } @@ -112,8 +144,22 @@ public boolean checkHash() { } catch (IOException e) { - Main.errorContext = "Error while checking the SHA1 hash."; - return false; + if (!this.hasTriedHashChecks) { + this.hasTriedHashChecks = true; + LogHelper.info("An error occured while checking the SHA1 hash, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + + return this.checkHash(); + } + else { + e.printStackTrace(); + Main.errorContext = "System error while checking the SHA1 hash."; + + return false; + } } } @@ -128,8 +174,22 @@ public boolean checkHash() { } catch (IOException e) { - Main.errorContext = "Error while checking the MD5 hash."; - return false; + if (!this.hasTriedHashChecks) { + this.hasTriedHashChecks = true; + LogHelper.info("An error occured while checking the MD5 hash, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + + return this.checkHash(); + } + else { + e.printStackTrace(); + Main.errorContext = "System error while checking the MD5 hash."; + + return false; + } } } @@ -147,8 +207,22 @@ public boolean checkHash() { } catch (IOException e) { - Main.errorContext = "Error while checking the CRC32 hash."; - return false; + if (!this.hasTriedHashChecks) { + this.hasTriedHashChecks = true; + LogHelper.info("An error occured while checking the CRC32 hash, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + + return this.checkHash(); + } + else { + e.printStackTrace(); + Main.errorContext = "System error while checking the CRC32 hash."; + + return false; + } } } @@ -170,11 +244,11 @@ public boolean checkCache() { String[] files = FileHelper.listDirectory("mcinstance-cache", false); for (String s : files) { - if (this.SHA512 != null && s.equalsIgnoreCase("SHA512-" + this.SHA512)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination, true); - if (this.SHA256 != null && s.equalsIgnoreCase("SHA256-" + this.SHA256)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination, true); - if (this.SHA1 != null && s.equalsIgnoreCase("SHA1-" + this.SHA1)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination, true); - if (this.MD5 != null && s.equalsIgnoreCase("MD5-" + this.MD5)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination, true); - if (this.CRC32 != null && s.equalsIgnoreCase("CRC32-" + this.CRC32)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination, true); + if (this.SHA512 != null && s.equalsIgnoreCase("SHA512-" + this.SHA512)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination); + if (this.SHA256 != null && s.equalsIgnoreCase("SHA256-" + this.SHA256)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination); + if (this.SHA1 != null && s.equalsIgnoreCase("SHA1-" + this.SHA1)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination); + if (this.MD5 != null && s.equalsIgnoreCase("MD5-" + this.MD5)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination); + if (this.CRC32 != null && s.equalsIgnoreCase("CRC32-" + this.CRC32)) return FileHelper.copy("mcinstance-cache" + File.separator + s, this.destination); } return false; diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/FileHelper.java b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/FileHelper.java index 7bc27d3..b040ce2 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/FileHelper.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/FileHelper.java @@ -9,20 +9,29 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.concurrent.TimeUnit; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.io.filefilter.TrueFileFilter; import org.apache.commons.io.FileUtils; + +/** +An helper class to do various file operations more easily. + +@author HRudyPlayZ +*/ @SuppressWarnings("UnusedReturnValue") public class FileHelper { -// This class aims to make file management less stupid than it is by default. + /** + Checks whether a given path is possible and valid regardless of the OS. + Used to validate a string path. Used by every method of the {@link FileHelper} class to skip IOExceptions. - private static boolean isInvalidPath (String path) { - // Makes sure a given path is possible, regardless of the OS. - // Every other method will use it to have valid paths (just to make sure it gets handled properly, in case the system doesn't return an IOException for those). - + @param path The string to validate. + @return Whether the path is valid or not. + */ + private static boolean isInvalidPath(String path) { String[] Forbidden = new String[] {"..", "<", ">", ":", "\"", "|", "?", "*"}; // Any path containing those characters will be denied. String[] ForbiddenNames = new String[]{"CON", "COM", "PRN", "AUX", "CLOCK$", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", @@ -49,36 +58,56 @@ private static boolean isInvalidPath (String path) { } - public static boolean createDirectory (String path) { - // Creates a new directory in the specified path (as a string). - // If the directory already exists, it doesn't do anything (just suceeds). - // If one of the parent directories doesn't exist, it creates them recursively. + /** + Creates a new directory at the specified path. + If the directory already exists, it doesn't do anything and succeeds. + If one of the parent directories doesn't exist, it creates them recursively. - try { + @param path The path where to create the directory (Must contain its name at the end). + @return Whether the operation succeeded or not. + */ + public static boolean createDirectory(String path) { + return createDirectory(path, false); + } - if (FileHelper.isInvalidPath(path)) return false; // Checks if the given path isn't valid. + private static boolean createDirectory(String path, boolean doneIOException) { + try { + if (isInvalidPath(path)) return false; // Checks if the given path isn't valid. Path realPath = Paths.get(path); Files.createDirectories(realPath); return true; } - catch (IOException e) { - return false; - } + if (!doneIOException) { + LogHelper.info("An error occured while creating the directory, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + return createDirectory(path, true); + } + else return false; + } } + /** + Creates a new empty file at the specified location (must contain the file name and extension, as a {@link String}). + If the file already exists, it overwrites the previous one. + If one of the parent directories doesn't exist or if the given path is actually a folder, it fails. - public static boolean createFile (String path) { - // Creates a new empty file at the given path (must contain its name, as a string). - // If the file already exists, it deletes the previous one. - // If one of the parent directories doesn't exist or if the given path is actually a folder, it fails. + @param path The path where to create the file (must contain its name and possibly extension at the end). + @return Whether the operation succeeded or not. + */ + public static boolean createFile(String path) { + return createFile(path, false); + } + private static boolean createFile(String path, boolean doneIOException) { try { - - if (FileHelper.isInvalidPath(path)) return false; // Checks if the given path isn't valid. + if (isInvalidPath(path)) return false; // Checks if the given path isn't valid. Path realPath = Paths.get(path); Files.deleteIfExists(realPath); @@ -88,62 +117,110 @@ public static boolean createFile (String path) { } catch (IOException e) { - return false; - } + if (!doneIOException) { + LogHelper.info("An error occured while creating the file, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + return createFile(path, true); + } + else return false; + } } + /** + Appends a specific list of line strings to the end of a given file. + In other words, this adds a list of lines, each represented by a {@link String} at the end of a file. + If the file doesn't exist, it creates it. + + @param path The path of the file to modify. + @param lines The list of lines to add to the end of the file. + @return Whether the operation succeeded or not. + */ + public static boolean appendFile(String path, String[] lines) { + return appendFile(path, lines, false); + } - public static boolean appendFile (String path, String[] lines) { - // Adds a list of lines to the end of a specific file. - // If the file doesn't exist, it creates it. - + private static boolean appendFile(String path, String[] lines, boolean doneIOException) { try { - - if (FileHelper.isInvalidPath(path)) return false; // Checks if the given path isn't valid. + if (isInvalidPath(path)) return false; // Checks if the given path isn't valid. Path realPath = Paths.get(path); - if (!Files.exists(realPath)) FileHelper.createFile(path); + if (!Files.exists(realPath)) createFile(path); Files.write(realPath, Arrays.asList(lines), StandardCharsets.UTF_8, StandardOpenOption.APPEND); return true; } catch (IOException e) { - return false; - } + if (!doneIOException) { + LogHelper.info("An error occured while changing the file, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + return appendFile(path, lines, true); + } + else return false; + } } + /** + Replaces a specific file's content with a given list of lines (as {@link String}s). + If the file doesn't exist, it creates it. - public static boolean overwriteFile (String path, String[] lines) { - // Replaces a specific file's content with a precise list of lines. - // If the file doesn't exist, it creates it. + @param path The path of the file to modify. + @param lines The list of lines that are present in the file. + @return Whether the operation succeeded or not. + */ + public static boolean overwriteFile(String path, String[] lines) { + return overwriteFile(path, lines, false); + } + private static boolean overwriteFile(String path, String[] lines, boolean doneIOException) { try { - - if (FileHelper.isInvalidPath(path)) return false; // Checks if the given path isn't valid. + if (isInvalidPath(path)) return false; // Checks if the given path isn't valid. Path realPath = Paths.get(path); - FileHelper.createFile(path); + createFile(path); Files.write(realPath, Arrays.asList(lines), StandardCharsets.UTF_8); return true; } catch (IOException e) { - return false; - } + if (!doneIOException) { + LogHelper.info("An error occured while overwriting the file, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + return overwriteFile(path, lines, true); + } + else return false; + } } - public static String[] listLines (String path) { - // Returns a list of each line in a specific file. + /** + Returns a list of lines as {@link String}s present inside a specific file. + It separates the file using the UTF-8 charset. And considers {@value \n} as the delimiter. + It returns an empty list if an {@link IOException} occurs. - try { + @param path The path of the file to read + @return The list of lines present in the file. + */ + public static String[] listLines(String path) { + return listLines(path, false); + } - if (FileHelper.isInvalidPath(path)) return new String[0]; // Checks if the given path isn't valid. + private static String[] listLines(String path, boolean doneIOException) { + try { + if (isInvalidPath(path)) return new String[0]; // Checks if the given path isn't valid. Path realPath = Paths.get(path); List list = Files.readAllLines(realPath); @@ -152,16 +229,31 @@ public static String[] listLines (String path) { } catch (IOException e) { - return new String[0]; + if (!doneIOException) { + LogHelper.info("An error occured while reading the file, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + + return listLines(path, true); + } + else return new String[0]; } } - public static String[] listDirectory(String path, boolean isRecursive) { - // Returns the list of files and folders in a specific directory. - // It can be recursive or not. + /** + Returns the list of files and folders inside a specific directory. + It can be set to either include the files inside the directories recursively or not. + It returns an empty list if the file doesn't exist or isn't a directory. - if (FileHelper.isInvalidPath(path)) return new String[0]; // Checks if the given path isn't valid. + @param path The path of the directory to read + @param isRecursive Whether the list should include the content of subfolders recursively. + @return The list of files and directories present in the specific path + */ + public static String[] listDirectory(String path, boolean isRecursive) { + if (isInvalidPath(path)) return new String[0]; // Checks if the given path isn't valid. String[] result; File file = new File(path); @@ -172,7 +264,6 @@ public static String[] listDirectory(String path, boolean isRecursive) { int cursor = 0; for (File i : list) { - if (!i.getPath().equals(path)) { result[cursor] = i.getPath(); result[cursor] = result[cursor].replace(path, ""); @@ -181,39 +272,41 @@ public static String[] listDirectory(String path, boolean isRecursive) { if (i.isDirectory()) result[cursor] += File.separator; cursor += 1; } - - - } } - else if (file.exists() && file.isDirectory()) result = file.list(); - else { - result = new String[0]; - } + else result = new String[0]; return result; } - - public static boolean copy (String source, String target, boolean replaceTarget) { - // Copies either a folder or a file to a specific location. - // Files need to have the target file name (with its extension) specified. - try { + /** + Copies an entire folder or a file to a specific location. + If the target path already exists, it deletes it. + Files need to have the target file name (and extension) specified too. - Path sourcePath = Paths.get(source); + @param source The path of the content to copy + @param target The path where to save the copied content + @return Whether the operation succeeded or not. + */ + public static boolean copy(String source, String target) { + return copy(source, target, false); + } - if (FileHelper.isInvalidPath(source)) return false; // Checks if the source path isn't valid. - if (FileHelper.isInvalidPath(target)) return false; // Checks if the target path isn't valid. + private static boolean copy(String source, String target, boolean doneIOException) { + try { + Path sourcePath = Paths.get(source); + if (isInvalidPath(source)) return false; // Checks if the source path isn't valid. + if (isInvalidPath(target)) return false; // Checks if the target path isn't valid. if (target.contains(File.separator)) { String path = target.substring(0, target.lastIndexOf(File.separator)); - if (!FileHelper.exists(path)) FileHelper.createDirectory(path); + if (!exists(path)) createDirectory(path); } - if (replaceTarget && FileHelper.exists(target)) FileHelper.delete(target); + if (exists(target)) delete(target); if (Files.isDirectory(sourcePath)) FileUtils.copyDirectory(new File(source), new File(target)); else Files.copy(sourcePath, Paths.get(target), StandardCopyOption.REPLACE_EXISTING); @@ -222,27 +315,50 @@ public static boolean copy (String source, String target, boolean replaceTarget) } catch (IOException e) { - LogHelper.info("You had an IOException while calling the copy method."); - e.printStackTrace(); - return false; + if (!doneIOException) { + LogHelper.info("An error occured while copying the file, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + + return copy(source, target, true); + } + else return false; } } - public static boolean move (String source, String target, boolean replaceTarget) { - // Move either a folder or a file to a specific location. - // Files need to have the target file name (with its extension) specified. - boolean result = FileHelper.copy(source, target, replaceTarget); - if (result) FileHelper.delete(source); + /** + Moves (copies and deletes) an entire folder or a file to a specific location. + If the the target files already exists, it deletes it. + It will only delete the original file if the copy operation is successful. + Files need to have the target file name (and extension) specified too. + + @param source The path of the content to copy and delete when successful. + @param target The path where to save the copied content. + @return Whether the operation succeeded or not. + */ + public static boolean move(String source, String target) { + boolean result = copy(source, target); + if (result) result = delete(source); return result; } - public static boolean delete (String path) { - // Deletes a specific file/folder from the disk. - // This method should not validate the path, just in case something managed to go through it and you need to delete something. + /** + Deletes an entire folder (recursively) or file from the disk. + This method doesn't validate the file path in case something manages to get through and needs to be deleted. + + @param path The path of the content to delete. + @return Whether the operation succeeded or not. + */ + public static boolean delete(String path) { + return delete(path, false); + } + private static boolean delete(String path, boolean doneIOException) { try { Path realPath = Paths.get(path); @@ -253,29 +369,43 @@ public static boolean delete (String path) { return true; } + catch (IOException e) { - return false; - } + if (!doneIOException) { + LogHelper.info("An error occured while deleting the file, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + return delete(path, true); + } + else return false; + } } - public static boolean exists(String path) { - // Returns a boolean indicating if a file/folder exists or not. - // Basically just acts as a lazy interface for the Files.exists method. - // I could have also used the File class, but both work fine. + /** + Checks if a file or folder exists. + Basically acts as a lazy interface for {@link Files#exists(Path, LinkOption...)}. - Path realPath = Paths.get(path); - return Files.exists(realPath); + @param path The path to check + @return Whether the path exists or not. + */ + public static boolean exists(String path) { + return Files.exists(Paths.get(path)); } - public static boolean isDirectory(String path) { - // Returns a boolean indicating if a path corresponds to a folder or not. - // Basically just acts as a lazy interface for the Files.isDirectory method. - // I could have also used the File class, but both work fine. + /** + Checks whether the given path corresponds to a folder or a file. + Returns true if the path is a folder and false otherwise. + Basically acts as a lazy interface for {@link Files#isDirectory(Path, LinkOption...)}. - Path realPath = Paths.get(path); - return Files.isDirectory(realPath); + @param path The path to check + @return Whether the path is a directory or not. + */ + public static boolean isDirectory(String path) { + return Files.isDirectory(Paths.get(path)); } } diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/LogHelper.java b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/LogHelper.java index 8d9fec6..0448be1 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/LogHelper.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/LogHelper.java @@ -7,29 +7,50 @@ import java.time.LocalDateTime; +/** +An helper class to print stuff to logs, also handles the mod's specific log file. + +@author HRudyPlayZ +*/ @SuppressWarnings("unused") public class LogHelper { -// This class is used to handle both printing to the game's logs, and outputing to mod log file. - public static void log(Level level, Object object) { - // Prints a certain object (probably a string) both in game log and the mod log. + /** + Prints a certain object (probably a {@link String}), both in the game logs and the mod logs. + @param level The {@link Level}'s level of importance. + @param object The object to print. + */ + public static void log(Level level, Object object) { appendToLog(level, object, false); FMLLog.log(ModProperties.NAME, level, String.valueOf(object)); } - public static void appendToLog(Level level, Object object, boolean skipFormat) { - // Adds a certain object (probably a string) to the mod log, the skipFormat boolean disables the formating with time and level. + /** + Prints a certain object (probably a {@link String}), to the mod's log file. + + @param level The {@link Level}'s level of importance. + @param object The object to print. + @param skipFormat Whether to disable the formating with time and level or not. + */ + public static void appendToLog(Level level, Object object, boolean skipFormat) { String text = String.valueOf(object); LocalDateTime now = LocalDateTime.now(); + if (!skipFormat) text = "[" + now.getHour() + ":" + now.getMinute() + ":" + now.getSecond() + "] " + level.toString() + ": " + text; + FileHelper.appendFile(Config.configFolder + "details.log", new String[]{text}); } - public static void verboseInfo (Object object) { - // Prints a certain object (probably a string) both in the game log if the verbose mode is enabled and in the mod log. + /** + Prints a certain object (probably a {@link String}) both to the mod's log and, if verbose mode is enabled, in the game's logs. + The importance level will be sent to {@link Level#INFO}. + + @param object + */ + public static void verboseInfo (Object object) { appendToLog(Level.INFO, object, false); if (Config.verboseMode) LogHelper.info(object); } diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/WebHelper.java b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/WebHelper.java index 6b4429d..24030ac 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/WebHelper.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/WebHelper.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.net.*; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import org.apache.commons.io.FileUtils; @@ -10,18 +11,30 @@ import com.hrudyplayz.mcinstanceloader.Config; import com.hrudyplayz.mcinstanceloader.Main; +/** +An helper class to download files from the internet. +@author HRudyPlayZ +*/ @SuppressWarnings("unused") public class WebHelper { -// This class will allow to download a file from the internet. // Defines the client properties, uses Twitch UserAgents to make every website work correctly as they should. public static String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) twitch-desktop-electron-platform/1.0.0 Chrome/73.0.3683.121 Electron/5.0.12 Safari/537.36 desklight/8.51.0"; public static String REFERER = "https://www.google.com"; - public static boolean downloadFile (String fileURL, String savePath) { - // Lets you download a file from a specific URL and save it to a location. + /** + Downloads a specific file from an internet address and saves it to a given location. + + @param fileURL The URL of the file. + @param savePath Where to save the downloaded file on the disk. + @return Whether the operation succeeded or not. + */ + public static boolean downloadFile(String fileURL, String savePath) { + return downloadFile(fileURL, savePath, false); + } + private static boolean downloadFile(String fileURL, String savePath, boolean doneIOException) { URL url; try { url = new URL(fileURL); @@ -54,14 +67,35 @@ public static boolean downloadFile (String fileURL, String savePath) { return true; } catch (IOException e) { - Main.errorContext = "There was an issue writing to file."; - return false; - } + if (!doneIOException) { + LogHelper.info("An error occured while downloading the file, trying again..."); + try { + TimeUnit.MILLISECONDS.sleep(1000); + } + catch (InterruptedException ignore) {} + return downloadFile(fileURL, savePath); + } + else { + Main.errorContext = "There was an issue writing to file."; + return false; + } + } } - public static boolean downloadFile (String fileURL, String savePath, String[] follows) { + /** + Downloads a specific file from an internet address and saves it to a given location. + It will first follow a list of buttons on the page to get to the final URL of the file. + To get to the final URL, this function will try to simulate a click on every HTML object with a given text, in order. + To do that, it will follow the href of such objects. + + @param fileURL The URL of the starting page. + @param savePath Where to save the downloaded file on the disk. + @param follows A list of buttons to follow in the right order. + @return Whether the operation succeeded or not. + */ + public static boolean downloadFile(String fileURL, String savePath, String[] follows) { // Lets you download a file from a specific URL and save it to a location. // Will follow any button with a text present in the follows list. diff --git a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/ZipHelper.java b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/ZipHelper.java index f186678..e63d3e3 100644 --- a/src/main/java/com/hrudyplayz/mcinstanceloader/utils/ZipHelper.java +++ b/src/main/java/com/hrudyplayz/mcinstanceloader/utils/ZipHelper.java @@ -2,53 +2,72 @@ import java.io.*; import java.util.List; +import java.util.zip.ZipEntry; import net.lingala.zip4j.ZipFile; import net.lingala.zip4j.model.FileHeader; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.model.enums.EncryptionMethod; -@SuppressWarnings("unused") -public class ZipHelper { -// This class aims to make zip files management much easier. -// It utilizes the zip4j library. I would've prefered to not rely on it, -// but Java is just too annoying when it comes to zip files, and i don't want to have to do -// this guy's work over again, so huge thanks to srikanth-lingala for providing a fairly -// licensed and easy to use library i could work with. -// The zip4j library is licensed under the Apache License 2.0. +/** +An helper class to make zip management easier. +It uses the Zip4J library (even though i would've prefered to not add an external dependency). +Java is pretty annoying with them by default, but srikanth-lingala made a massive work to +provide an easy to use and fairly licensed library, so huge thanks to him. +Current Zip4J version: 2.11.1 +The Zip4J library is licensed under the Apache License 2.0. - public static boolean zip (String input, String outputZip, String password, boolean doNotOverwrite, boolean putContentInSubdirectory) { - // Lets you archive a folder's content into a zip file. - // The folder itself is only included in the archive if you set putContentInSubdirectory to true. +@author srikanth-lingala +@author HRudyPlayZ +*/ +@SuppressWarnings("unused") +public class ZipHelper { + /** + Archives a folder's content or a file into a zip file saved at a given location. + If the target location already exists and isn't a valid zip file, it will delete it first. + + @param source The path of the content to archive. + @param saveLocation The path where to save the outputed zip. + @param putContentInSubdirectory If the source content is a folder, whether the function should put the content inside a subdirectory with the folder's name or not. + @param overwriteFile If set to true, this will delete the existing file instead of adding to it. + @param password The password of the zip, set to null to disable it. + @return Whether the operation succeeded or not. + */ + public static boolean zip(String source, String saveLocation, boolean putContentInSubdirectory, boolean overwriteFile, String password) { try { - if ((!doNotOverwrite) && (!FileHelper.isDirectory(outputZip)) && FileHelper.exists(outputZip)) FileHelper.delete(outputZip); + if (!FileHelper.exists(source)) return false; // If the source doesn't exist, immediately return false. + + // Deletes the save location if it already exists and isn't a valid zip file or the overwriteFile argument is set to true. + if (FileHelper.exists(saveLocation) && (!isValidZip(saveLocation) || overwriteFile)) FileHelper.delete(saveLocation); ZipFile zipFile; ZipParameters zipParameters = new ZipParameters(); - if (password != null) { + + if (password != null) { // Sets the zip password if added. zipParameters.setEncryptFiles(true); zipParameters.setEncryptionMethod(EncryptionMethod.AES); - zipFile = new ZipFile(outputZip, password.toCharArray()); + zipFile = new ZipFile(saveLocation, password.toCharArray()); } - else zipFile = new ZipFile(outputZip); + else zipFile = new ZipFile(saveLocation); - if (!new File(input).isDirectory()) { - zipFile.addFile(input, zipParameters); + if (!FileHelper.isDirectory(source)) { // If the source is just a file, we zip this file and don't continue further. + zipFile.addFile(source, zipParameters); return true; } - if (putContentInSubdirectory) zipFile.addFolder(new File(input)); - else { - String[] fileList = FileHelper.listDirectory(input, false); + // Otherwise, we prepare to add a folder's content to the zip instead. + if (!source.endsWith(File.separator)) source += File.separator; - if (!input.endsWith(File.separator)) input += File.separator; + if (putContentInSubdirectory) zipFile.addFolder(new File(source)); + else { + String[] fileList = FileHelper.listDirectory(source, false); for (int i = 0; i < fileList.length; i += 1) { - fileList[i] = input + fileList[i]; + fileList[i] = source + fileList[i]; File file = new File(fileList[i]); if (file.isDirectory()) zipFile.addFolder(file, zipParameters); @@ -64,87 +83,122 @@ public static boolean zip (String input, String outputZip, String password, bool } } - - public static boolean zip (String input, String outputZip, String password, boolean doNotOverwrite) { - // Lets you archive a folder's content into a zip file. - // This makes the putContentInDirectory setting an option and not a required argument. - - return zip(input, outputZip, password, doNotOverwrite, false); + /** + Archives a folder's content or a file into a zip file saved at a given location. + If the target location already exists and isn't a valid zip file, it will delete it first. + The outputed zip won't have any password. + + @param source The path of the content to archive. + @param saveLocation The path where to save the outputed zip. + @param putContentInSubdirectory If the source content is a folder, whether the function should put the content inside a subdirectory with the folder's name or not. + @param overwriteFile If set to true, this will delete the existing file instead of adding to it. + @return Whether the operation succeeded or not. + */ + public static boolean zip(String source, String saveLocation, boolean putContentInSubdirectory, boolean overwriteFile) { + return zip(source, saveLocation, putContentInSubdirectory, overwriteFile, null); } - public static boolean zip (String input, String outputZip, String password) { - // Lets you archive a folder's content into a zip file. - // This makes the doNotOverwrite setting an option and not a required argument. - - return zip(input, outputZip, password, false); + /** + Archives a folder's content or a file into a zip file saved at a given location. + If the target location already exists and isn't a valid zip file, it will delete it first. + If a zip file already exists, this will try to add to it instead of overwriting it. The outputed zip won't have any password. + + @param source The path of the content to archive. + @param saveLocation The path where to save the outputed zip. + @param putContentInSubdirectory If the source content is a folder, whether the function should put the content inside a subdirectory with the folder's name or not. + @return Whether the operation succeeded or not. + */ + public static boolean zip(String source, String saveLocation, boolean putContentInSubdirectory) { + return zip(source, saveLocation, putContentInSubdirectory, false, null); } - public static boolean zip (String input, String outputZip) { - // Lets you archive a folder's content into a zip file. - // This makes the password setting an option and not a required argument. - return zip(input, outputZip, null); - } - - - public static boolean extract(String zipFile, String destinationFolder, String password, String fileToExtract) { - // Lets you extract a zip archive into a specific folder. + /** + Extracts a given list of files from a zip file into a specified location. + @param zipFile The path of the zip file to extract from. + @param destination The path where to save the extracted content. + @param password The password of the zip if there's one, set to null otherwise. + @param filesToExtract A list of files to extract, uses "/" for the folder separator. Add a "/" after folder names. + @return Whether the operation succeeded or not. + */ + public static boolean extract(String zipFile, String destination, String password, String[] filesToExtract) { try { ZipFile file; if (password != null) file = new ZipFile(zipFile, password.toCharArray()); else file = new ZipFile(zipFile); - if (fileToExtract != null) file.extractFile(fileToExtract, destinationFolder); - else file.extractAll(destinationFolder); + if (filesToExtract != null) { + for (String s : filesToExtract) file.extractFile(s, destination); + } + else file.extractAll(destination); return true; } - catch (net.lingala.zip4j.exception.ZipException e) { return false; } - } - public static boolean extract(String zipFile, String destinationFolder, String password) { - // Lets you extract a zip archive into a specific folder. - // This makes the fileToExtract setting optional. + /** + Extracts an entire zip file's content into a specified location. - return extract(zipFile, destinationFolder, password, null); + @param zipFile The path of the zip file to extract from. + @param destination The path where to save the extracted content. + @param password The password of the zip if there's one, set to null otherwise. + @return Whether the operation succeeded or not. + */ + public static boolean extract(String zipFile, String destination, String password) { + return extract(zipFile, destination, password, null); } - public static boolean extract(String zipFile, String destinationFolder) { - // Lets you extract a zip archive into a specific folder. - // This makes the password setting optional. + /** + Extracts an entire unencrypted (passwordless) zip file's content into a specified location. - return extract(zipFile, destinationFolder, null); + @param zipFile The path of the zip file to extract from. + @param destination The path where to save the extracted content. + @return Whether the operation succeeded or not. + */ + public static boolean extract(String zipFile, String destination) { + return extract(zipFile, destination, null); } - public static boolean isEncrypted (String zipFile) { - // Lets you know if a specific archive has a password. + /** + Checks whether a given zip file is encrypted (has a password) or not. + Will return false if the given path isn't a zip file. + @param zipFile The path of the zip file to check. + @return Whether the zip file is encrypted (has a password) or not. + */ + public static boolean isEncrypted(String zipFile) { try { return new ZipFile(zipFile).isEncrypted(); } - catch (net.lingala.zip4j.exception.ZipException e) { return false; } } + /** + Checks whether a given path is a valid zip file or not. - public static boolean isValidZip (String zipFile) { - // Lets you know if a specific archive has a password. - + @param zipFile The path to check. + @return Whether the given path is a valid zip file or not. + */ + public static boolean isValidZip(String zipFile) { return new ZipFile(zipFile).isValidZipFile(); } - public static String[] listZip (String zipFile) { - // Lets you know the list of files included in a specific zip file. + /** + Returns the list of files present inside a given zip file. + Will return an empty list if an error happens or the given path isn't a valid zip file. + @param zipFile The path of the zip file to read. + @return The list of files inside the zip file. + */ + public static String[] listZip(String zipFile) { try { List fileHeaders = new ZipFile(zipFile).getFileHeaders(); String[] result = new String[fileHeaders.size()]; @@ -155,24 +209,27 @@ public static String[] listZip (String zipFile) { return result; } - catch (net.lingala.zip4j.exception.ZipException e) { return new String[0]; } } - public static boolean delete (String zipFile, String file) { - // Lets you delete a specific file/folder in a zip archive. - // Add a "/" after folder names. + /** + Deletes a specific list of files or folders from a given zip file. + To delete a folder, Add a "/" after its name. + @param zipFile The path of the zip file to modify. + @param filesToDelete The list of files to delete from the archive. + @return Whether the operation succeeded or not. + */ + public static boolean delete(String zipFile, String[] filesToDelete) { try { - new ZipFile(zipFile).removeFile(file); + ZipFile file = new ZipFile(zipFile); + for (String s : filesToDelete) file.removeFile(s); return true; } - catch (net.lingala.zip4j.exception.ZipException e) { - LogHelper.error(e.getMessage()); return false; } } diff --git a/src/main/java/net/lingala/zip4j/ZipFile.java b/src/main/java/net/lingala/zip4j/ZipFile.java index 6ad271f..1867486 100644 --- a/src/main/java/net/lingala/zip4j/ZipFile.java +++ b/src/main/java/net/lingala/zip4j/ZipFile.java @@ -99,6 +99,7 @@ public class ZipFile implements Closeable { private ExecutorService executorService; private int bufferSize = InternalZipConstants.BUFF_SIZE; private List openInputStreams = new ArrayList<>(); + private boolean useUtf8CharsetForPasswords = InternalZipConstants.USE_UTF8_FOR_PASSWORD_ENCODING_DECODING; /** * Creates a new ZipFile instance with the zip file at the location specified in zipFile. @@ -1233,6 +1234,14 @@ public String toString() { } private Zip4jConfig buildConfig() { - return new Zip4jConfig(charset, bufferSize); + return new Zip4jConfig(charset, bufferSize, useUtf8CharsetForPasswords); + } + + public boolean isUseUtf8CharsetForPasswords() { + return useUtf8CharsetForPasswords; + } + + public void setUseUtf8CharsetForPasswords(boolean useUtf8CharsetForPasswords) { + this.useUtf8CharsetForPasswords = useUtf8CharsetForPasswords; } } diff --git a/src/main/java/net/lingala/zip4j/crypto/AESDecrypter.java b/src/main/java/net/lingala/zip4j/crypto/AESDecrypter.java index 2daa7e3..accb35c 100644 --- a/src/main/java/net/lingala/zip4j/crypto/AESDecrypter.java +++ b/src/main/java/net/lingala/zip4j/crypto/AESDecrypter.java @@ -40,21 +40,22 @@ public class AESDecrypter implements Decrypter { private byte[] iv; private byte[] counterBlock; - public AESDecrypter(AESExtraDataRecord aesExtraDataRecord, char[] password, byte[] salt, byte[] passwordVerifier) throws ZipException { + public AESDecrypter(AESExtraDataRecord aesExtraDataRecord, char[] password, byte[] salt, + byte[] passwordVerifier, boolean useUtf8ForPassword) throws ZipException { iv = new byte[AES_BLOCK_SIZE]; counterBlock = new byte[AES_BLOCK_SIZE]; - init(salt, passwordVerifier, password, aesExtraDataRecord); + init(salt, passwordVerifier, password, aesExtraDataRecord, useUtf8ForPassword); } - private void init(byte[] salt, byte[] passwordVerifier, char[] password, AESExtraDataRecord aesExtraDataRecord) - throws ZipException { + private void init(byte[] salt, byte[] passwordVerifier, char[] password, + AESExtraDataRecord aesExtraDataRecord, boolean useUtf8ForPassword) throws ZipException { if (password == null || password.length <= 0) { throw new ZipException("empty or null password provided for AES decryption", WRONG_PASSWORD); } final AesKeyStrength aesKeyStrength = aesExtraDataRecord.getAesKeyStrength(); - final byte[] derivedKey = AesCipherUtil.derivePasswordBasedKey(salt, password, aesKeyStrength); + final byte[] derivedKey = AesCipherUtil.derivePasswordBasedKey(salt, password, aesKeyStrength, useUtf8ForPassword); final byte[] derivedPasswordVerifier = AesCipherUtil.derivePasswordVerifier(derivedKey, aesKeyStrength); if (!Arrays.equals(passwordVerifier, derivedPasswordVerifier)) { throw new ZipException("Wrong Password", ZipException.Type.WRONG_PASSWORD); diff --git a/src/main/java/net/lingala/zip4j/crypto/AESEncrypter.java b/src/main/java/net/lingala/zip4j/crypto/AESEncrypter.java index 9776ba8..5780877 100644 --- a/src/main/java/net/lingala/zip4j/crypto/AESEncrypter.java +++ b/src/main/java/net/lingala/zip4j/crypto/AESEncrypter.java @@ -49,7 +49,7 @@ public class AESEncrypter implements Encrypter { private byte[] derivedPasswordVerifier; private byte[] saltBytes; - public AESEncrypter(char[] password, AesKeyStrength aesKeyStrength) throws ZipException { + public AESEncrypter(char[] password, AesKeyStrength aesKeyStrength, boolean useUtf8ForPassword) throws ZipException { if (password == null || password.length == 0) { throw new ZipException("input password is empty or null"); } @@ -61,12 +61,12 @@ public AESEncrypter(char[] password, AesKeyStrength aesKeyStrength) throws ZipEx this.finished = false; counterBlock = new byte[AES_BLOCK_SIZE]; iv = new byte[AES_BLOCK_SIZE]; - init(password, aesKeyStrength); + init(password, aesKeyStrength, useUtf8ForPassword); } - private void init(char[] password, AesKeyStrength aesKeyStrength) throws ZipException { + private void init(char[] password, AesKeyStrength aesKeyStrength, boolean useUtf8ForPassword) throws ZipException { saltBytes = generateSalt(aesKeyStrength.getSaltLength()); - byte[] derivedKey = derivePasswordBasedKey(saltBytes, password, aesKeyStrength); + byte[] derivedKey = derivePasswordBasedKey(saltBytes, password, aesKeyStrength, useUtf8ForPassword); derivedPasswordVerifier = derivePasswordVerifier(derivedKey, aesKeyStrength); aesEngine = getAESEngine(derivedKey, aesKeyStrength); mac = getMacBasedPRF(derivedKey, aesKeyStrength); diff --git a/src/main/java/net/lingala/zip4j/crypto/AesCipherUtil.java b/src/main/java/net/lingala/zip4j/crypto/AesCipherUtil.java index 7a23764..7d1f28f 100644 --- a/src/main/java/net/lingala/zip4j/crypto/AesCipherUtil.java +++ b/src/main/java/net/lingala/zip4j/crypto/AesCipherUtil.java @@ -24,14 +24,16 @@ public class AesCipherUtil { * @return Derived Password-Based Key * @throws ZipException Thrown when Derived Key is not valid */ - public static byte[] derivePasswordBasedKey(final byte[] salt, final char[] password, final AesKeyStrength aesKeyStrength) throws ZipException { + public static byte[] derivePasswordBasedKey(final byte[] salt, final char[] password, + final AesKeyStrength aesKeyStrength, + final boolean useUtf8ForPassword) throws ZipException { final PBKDF2Parameters parameters = new PBKDF2Parameters(AES_MAC_ALGORITHM, AES_HASH_CHARSET, salt, AES_HASH_ITERATIONS); final PBKDF2Engine engine = new PBKDF2Engine(parameters); final int keyLength = aesKeyStrength.getKeyLength(); final int macLength = aesKeyStrength.getMacLength(); final int derivedKeyLength = keyLength + macLength + AES_PASSWORD_VERIFIER_LENGTH; - final byte[] derivedKey = engine.deriveKey(password, derivedKeyLength); + final byte[] derivedKey = engine.deriveKey(password, derivedKeyLength, useUtf8ForPassword); if (derivedKey != null && derivedKey.length == derivedKeyLength) { return derivedKey; } else { diff --git a/src/main/java/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Engine.java b/src/main/java/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Engine.java index 5bd23c8..cddad0b 100644 --- a/src/main/java/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Engine.java +++ b/src/main/java/net/lingala/zip4j/crypto/PBKDF2/PBKDF2Engine.java @@ -37,17 +37,13 @@ public PBKDF2Engine(PBKDF2Parameters parameters, PRF prf) { this.prf = prf; } - public byte[] deriveKey(char[] inputPassword) { - return deriveKey(inputPassword, 0); - } - - public byte[] deriveKey(char[] inputPassword, int dkLen) { + public byte[] deriveKey(char[] inputPassword, int dkLen, boolean useUtf8ForPassword) { byte p[]; if (inputPassword == null) { throw new NullPointerException(); } - p = convertCharArrayToByteArray(inputPassword); + p = convertCharArrayToByteArray(inputPassword, useUtf8ForPassword); assertPRF(p); if (dkLen == 0) { @@ -56,24 +52,6 @@ public byte[] deriveKey(char[] inputPassword, int dkLen) { return PBKDF2(prf, parameters.getSalt(), parameters.getIterationCount(), dkLen); } - public boolean verifyKey(char[] inputPassword) { - byte[] referenceKey = getParameters().getDerivedKey(); - if (referenceKey == null || referenceKey.length == 0) { - return false; - } - byte[] inputKey = deriveKey(inputPassword, referenceKey.length); - - if (inputKey == null || inputKey.length != referenceKey.length) { - return false; - } - for (int i = 0; i < inputKey.length; i++) { - if (inputKey[i] != referenceKey[i]) { - return false; - } - } - return true; - } - private void assertPRF(byte[] P) { if (prf == null) { prf = new MacBasedPRF(parameters.getHashAlgorithm()); @@ -81,10 +59,6 @@ private void assertPRF(byte[] P) { prf.init(P); } - public PRF getPseudoRandomFunction() { - return prf; - } - private byte[] PBKDF2(PRF prf, byte[] S, int c, int dkLen) { if (S == null) { S = new byte[0]; diff --git a/src/main/java/net/lingala/zip4j/crypto/StandardDecrypter.java b/src/main/java/net/lingala/zip4j/crypto/StandardDecrypter.java index a21061b..a9c1765 100644 --- a/src/main/java/net/lingala/zip4j/crypto/StandardDecrypter.java +++ b/src/main/java/net/lingala/zip4j/crypto/StandardDecrypter.java @@ -26,9 +26,10 @@ public class StandardDecrypter implements Decrypter { private ZipCryptoEngine zipCryptoEngine; - public StandardDecrypter(char[] password, long crc, long lastModifiedFileTime, byte[] headerBytes) throws ZipException { + public StandardDecrypter(char[] password, long crc, long lastModifiedFileTime, + byte[] headerBytes, boolean useUtf8ForPassword) throws ZipException { this.zipCryptoEngine = new ZipCryptoEngine(); - init(headerBytes, password, lastModifiedFileTime, crc); + init(headerBytes, password, lastModifiedFileTime, crc, useUtf8ForPassword); } public int decryptData(byte[] buff, int start, int len) throws ZipException { @@ -46,12 +47,13 @@ public int decryptData(byte[] buff, int start, int len) throws ZipException { return len; } - private void init(byte[] headerBytes, char[] password, long lastModifiedFileTime, long crc) throws ZipException { + private void init(byte[] headerBytes, char[] password, long lastModifiedFileTime, long crc, + boolean useUtf8ForPassword) throws ZipException { if (password == null || password.length <= 0) { throw new ZipException("Wrong password!", ZipException.Type.WRONG_PASSWORD); } - zipCryptoEngine.initKeys(password); + zipCryptoEngine.initKeys(password, useUtf8ForPassword); int result = headerBytes[0]; for (int i = 0; i < STD_DEC_HDR_SIZE; i++) { diff --git a/src/main/java/net/lingala/zip4j/crypto/StandardEncrypter.java b/src/main/java/net/lingala/zip4j/crypto/StandardEncrypter.java index b179fa9..ad3a699 100644 --- a/src/main/java/net/lingala/zip4j/crypto/StandardEncrypter.java +++ b/src/main/java/net/lingala/zip4j/crypto/StandardEncrypter.java @@ -28,18 +28,18 @@ public class StandardEncrypter implements Encrypter { private final ZipCryptoEngine zipCryptoEngine = new ZipCryptoEngine(); private byte[] headerBytes; - public StandardEncrypter(char[] password, long key) throws ZipException { - init(password, key); + public StandardEncrypter(char[] password, long key, boolean useUtf8ForPassword) throws ZipException { + init(password, key, useUtf8ForPassword); } - private void init(char[] password, long key) throws ZipException { + private void init(char[] password, long key, boolean useUtf8ForPassword) throws ZipException { if (password == null || password.length <= 0) { throw new ZipException("input password is null or empty, cannot initialize standard encrypter"); } - zipCryptoEngine.initKeys(password); + zipCryptoEngine.initKeys(password, useUtf8ForPassword); headerBytes = generateRandomBytes(); // Initialize again since the generated bytes were encrypted. - zipCryptoEngine.initKeys(password); + zipCryptoEngine.initKeys(password, useUtf8ForPassword); headerBytes[STD_DEC_HDR_SIZE - 1] = (byte) ((key >>> 24)); headerBytes[STD_DEC_HDR_SIZE - 2] = (byte) ((key >>> 16)); diff --git a/src/main/java/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java b/src/main/java/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java index a2d6b83..0f53d7d 100644 --- a/src/main/java/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java +++ b/src/main/java/net/lingala/zip4j/crypto/engine/ZipCryptoEngine.java @@ -37,11 +37,11 @@ public class ZipCryptoEngine { } } - public void initKeys(char[] password) { + public void initKeys(char[] password, boolean useUtf8ForPassword) { keys[0] = 305419896; keys[1] = 591751049; keys[2] = 878082192; - byte[] bytes = convertCharArrayToByteArray(password); + byte[] bytes = convertCharArrayToByteArray(password, useUtf8ForPassword); for (byte b : bytes) { updateKeys((byte) (b & 0xff)); } diff --git a/src/main/java/net/lingala/zip4j/headers/FileHeaderFactory.java b/src/main/java/net/lingala/zip4j/headers/FileHeaderFactory.java index fbbaae2..b26dcea 100644 --- a/src/main/java/net/lingala/zip4j/headers/FileHeaderFactory.java +++ b/src/main/java/net/lingala/zip4j/headers/FileHeaderFactory.java @@ -54,12 +54,7 @@ public FileHeader generateFileHeader(ZipParameters zipParameters, boolean isSpli fileHeader.setFileName(fileName); fileHeader.setFileNameLength(determineFileNameLength(fileName, charset)); fileHeader.setDiskNumberStart(isSplitZip ? currentDiskNumberStart : 0); - - if (zipParameters.getLastModifiedFileTime() > 0) { - fileHeader.setLastModifiedTime(Zip4jUtil.epochToExtendedDosTime(zipParameters.getLastModifiedFileTime())); - } else { - fileHeader.setLastModifiedTime(Zip4jUtil.epochToExtendedDosTime(System.currentTimeMillis())); - } + fileHeader.setLastModifiedTime(Zip4jUtil.epochToExtendedDosTime(zipParameters.getLastModifiedFileTime())); boolean isDirectory = isZipEntryDirectory(fileName); fileHeader.setDirectory(isDirectory); diff --git a/src/main/java/net/lingala/zip4j/headers/HeaderReader.java b/src/main/java/net/lingala/zip4j/headers/HeaderReader.java index 466cc42..e60da7f 100644 --- a/src/main/java/net/lingala/zip4j/headers/HeaderReader.java +++ b/src/main/java/net/lingala/zip4j/headers/HeaderReader.java @@ -19,6 +19,7 @@ import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.io.inputstream.NumberedSplitRandomAccessFile; import net.lingala.zip4j.model.AESExtraDataRecord; +import net.lingala.zip4j.model.AbstractFileHeader; import net.lingala.zip4j.model.CentralDirectory; import net.lingala.zip4j.model.DataDescriptor; import net.lingala.zip4j.model.DigitalSignature; @@ -193,7 +194,7 @@ private CentralDirectory readCentralDirectory(RandomAccessFile zip4jRaf, RawIO r String fileName = decodeStringWithCharset(fileNameBuff, fileHeader.isFileNameUTF8Encoded(), charset); fileHeader.setFileName(fileName); } else { - fileHeader.setFileName(null); + throw new ZipException("Invalid entry name in file header"); } fileHeader.setDirectory(isDirectory(fileHeader.getExternalFileAttributes(), fileHeader.getFileName())); @@ -558,7 +559,7 @@ public LocalFileHeader readLocalFileHeader(InputStream inputStream, Charset char localFileHeader.setFileName(fileName); localFileHeader.setDirectory(fileName.endsWith("/") || fileName.endsWith("\\")); } else { - localFileHeader.setFileName(null); + throw new ZipException("Invalid entry name in local file header"); } readExtraDataRecords(inputStream, localFileHeader); @@ -612,7 +613,7 @@ public DataDescriptor readDataDescriptor(InputStream inputStream, boolean isZip6 return dataDescriptor; } - private void readAesExtraDataRecord(FileHeader fileHeader, RawIO rawIO) throws ZipException { + private void readAesExtraDataRecord(AbstractFileHeader fileHeader, RawIO rawIO) throws ZipException { if (fileHeader.getExtraDataRecords() == null || fileHeader.getExtraDataRecords().size() <= 0) { return; } @@ -624,18 +625,6 @@ private void readAesExtraDataRecord(FileHeader fileHeader, RawIO rawIO) throws Z } } - private void readAesExtraDataRecord(LocalFileHeader localFileHeader, RawIO rawIO) throws ZipException { - if (localFileHeader.getExtraDataRecords() == null || localFileHeader.getExtraDataRecords().size() <= 0) { - return; - } - - AESExtraDataRecord aesExtraDataRecord = readAesExtraDataRecord(localFileHeader.getExtraDataRecords(), rawIO); - if (aesExtraDataRecord != null) { - localFileHeader.setAesExtraDataRecord(aesExtraDataRecord); - localFileHeader.setEncryptionMethod(EncryptionMethod.AES); - } - } - private AESExtraDataRecord readAesExtraDataRecord(List extraDataRecords, RawIO rawIO) throws ZipException { @@ -650,7 +639,8 @@ private AESExtraDataRecord readAesExtraDataRecord(List extraDat if (extraDataRecord.getHeader() == HeaderSignature.AES_EXTRA_DATA_RECORD.getValue()) { - if (extraDataRecord.getData() == null) { + byte[] aesExtraDataRecordBytes = extraDataRecord.getData(); + if (aesExtraDataRecordBytes == null || aesExtraDataRecordBytes.length != 7) { throw new ZipException("corrupt AES extra data records"); } diff --git a/src/main/java/net/lingala/zip4j/headers/HeaderUtil.java b/src/main/java/net/lingala/zip4j/headers/HeaderUtil.java index 9a141ee..4a1271f 100644 --- a/src/main/java/net/lingala/zip4j/headers/HeaderUtil.java +++ b/src/main/java/net/lingala/zip4j/headers/HeaderUtil.java @@ -118,7 +118,7 @@ private static FileHeader getFileHeaderWithExactMatch(ZipModel zipModel, String continue; } - if (fileName.equalsIgnoreCase(fileNameForHdr)) { + if (fileName.equals(fileNameForHdr)) { return fileHeader; } } diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/AesCipherInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/AesCipherInputStream.java index a3cb2ef..25d55cf 100644 --- a/src/main/java/net/lingala/zip4j/io/inputstream/AesCipherInputStream.java +++ b/src/main/java/net/lingala/zip4j/io/inputstream/AesCipherInputStream.java @@ -28,13 +28,15 @@ class AesCipherInputStream extends CipherInputStream { private int aes16ByteBlockReadLength = 0; public AesCipherInputStream(ZipEntryInputStream zipEntryInputStream, LocalFileHeader localFileHeader, - char[] password, int bufferSize) throws IOException { - super(zipEntryInputStream, localFileHeader, password, bufferSize); + char[] password, int bufferSize, boolean useUtf8ForPassword) throws IOException { + super(zipEntryInputStream, localFileHeader, password, bufferSize, useUtf8ForPassword); } @Override - protected AESDecrypter initializeDecrypter(LocalFileHeader localFileHeader, char[] password) throws IOException { - return new AESDecrypter(localFileHeader.getAesExtraDataRecord(), password, getSalt(localFileHeader), getPasswordVerifier()); + protected AESDecrypter initializeDecrypter(LocalFileHeader localFileHeader, char[] password, + boolean useUtf8ForPassword) throws IOException { + return new AESDecrypter(localFileHeader.getAesExtraDataRecord(), password, getSalt(localFileHeader), + getPasswordVerifier(), useUtf8ForPassword); } @Override @@ -158,6 +160,11 @@ private byte[] getSalt(LocalFileHeader localFileHeader) throws IOException { } AESExtraDataRecord aesExtraDataRecord = localFileHeader.getAesExtraDataRecord(); + + if (aesExtraDataRecord.getAesKeyStrength() == null) { + throw new IOException("Invalid aes key strength in aes extra data record"); + } + byte[] saltBytes = new byte[aesExtraDataRecord.getAesKeyStrength().getSaltLength()]; readRaw(saltBytes); return saltBytes; diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/CipherInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/CipherInputStream.java index 1edb20c..78caab6 100644 --- a/src/main/java/net/lingala/zip4j/io/inputstream/CipherInputStream.java +++ b/src/main/java/net/lingala/zip4j/io/inputstream/CipherInputStream.java @@ -1,7 +1,6 @@ package net.lingala.zip4j.io.inputstream; import net.lingala.zip4j.crypto.Decrypter; -import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.model.LocalFileHeader; import net.lingala.zip4j.model.enums.CompressionMethod; import net.lingala.zip4j.util.Zip4jUtil; @@ -20,9 +19,9 @@ abstract class CipherInputStream extends InputStream { private LocalFileHeader localFileHeader; public CipherInputStream(ZipEntryInputStream zipEntryInputStream, LocalFileHeader localFileHeader, - char[] password, int bufferSize) throws IOException { + char[] password, int bufferSize, boolean useUtf8ForPassword) throws IOException { this.zipEntryInputStream = zipEntryInputStream; - this.decrypter = initializeDecrypter(localFileHeader, password); + this.decrypter = initializeDecrypter(localFileHeader, password, useUtf8ForPassword); this.localFileHeader = localFileHeader; if (Zip4jUtil.getCompressionMethod(localFileHeader).equals(CompressionMethod.DEFLATE)) { @@ -93,5 +92,6 @@ public LocalFileHeader getLocalFileHeader() { return localFileHeader; } - protected abstract T initializeDecrypter(LocalFileHeader localFileHeader, char[] password) throws IOException, ZipException; + protected abstract T initializeDecrypter(LocalFileHeader localFileHeader, char[] password, + boolean useUtf8ForPassword) throws IOException; } diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/NoCipherInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/NoCipherInputStream.java index 1883c94..18a3ecd 100644 --- a/src/main/java/net/lingala/zip4j/io/inputstream/NoCipherInputStream.java +++ b/src/main/java/net/lingala/zip4j/io/inputstream/NoCipherInputStream.java @@ -9,11 +9,11 @@ class NoCipherInputStream extends CipherInputStream { public NoCipherInputStream(ZipEntryInputStream zipEntryInputStream, LocalFileHeader localFileHeader, char[] password, int bufferSize) throws IOException { - super(zipEntryInputStream, localFileHeader, password, bufferSize); + super(zipEntryInputStream, localFileHeader, password, bufferSize, true); } @Override - protected Decrypter initializeDecrypter(LocalFileHeader localFileHeader, char[] password) { + protected Decrypter initializeDecrypter(LocalFileHeader localFileHeader, char[] password, boolean useUtf8ForPassword) { return new NoDecrypter(); } diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/NumberedSplitFileInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/NumberedSplitFileInputStream.java new file mode 100644 index 0000000..d715e89 --- /dev/null +++ b/src/main/java/net/lingala/zip4j/io/inputstream/NumberedSplitFileInputStream.java @@ -0,0 +1,47 @@ +package net.lingala.zip4j.io.inputstream; + +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.enums.RandomAccessFileMode; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * A split input stream for zip file split with 7-zip. They end with .zip.001, .zip.002, etc + */ +public class NumberedSplitFileInputStream extends SplitFileInputStream { + + private RandomAccessFile randomAccessFile; + + public NumberedSplitFileInputStream(File zipFile) throws IOException { + this.randomAccessFile = new NumberedSplitRandomAccessFile(zipFile, RandomAccessFileMode.READ.getValue()); + } + + @Override + public int read() throws IOException { + return randomAccessFile.read(); + } + + @Override + public int read(byte[] b) throws IOException { + return this.read(b,0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return randomAccessFile.read(b, off, len); + } + + @Override + public void prepareExtractionForFileHeader(FileHeader fileHeader) throws IOException { + randomAccessFile.seek(fileHeader.getOffsetLocalHeader()); + } + + @Override + public void close() throws IOException { + if (randomAccessFile != null) { + randomAccessFile.close(); + } + } +} diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/NumberedSplitRandomAccessFile.java b/src/main/java/net/lingala/zip4j/io/inputstream/NumberedSplitRandomAccessFile.java index 6c723c0..432a148 100644 --- a/src/main/java/net/lingala/zip4j/io/inputstream/NumberedSplitRandomAccessFile.java +++ b/src/main/java/net/lingala/zip4j/io/inputstream/NumberedSplitRandomAccessFile.java @@ -119,6 +119,14 @@ public long length() throws IOException { return randomAccessFile.length(); } + @Override + public void close() throws IOException { + if (randomAccessFile != null) { + randomAccessFile.close(); + } + super.close(); + } + public void seekInCurrentPart(long pos) throws IOException { randomAccessFile.seek(pos); } diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/SplitFileInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/SplitFileInputStream.java new file mode 100644 index 0000000..ce361d8 --- /dev/null +++ b/src/main/java/net/lingala/zip4j/io/inputstream/SplitFileInputStream.java @@ -0,0 +1,13 @@ +package net.lingala.zip4j.io.inputstream; + +import net.lingala.zip4j.model.FileHeader; + +import java.io.IOException; +import java.io.InputStream; + +// Even though this abstract class has only abstract method definitions, it is not implemented as an interface because +// implementations of this class has to be used as an inputstream to ZipInputStream +public abstract class SplitFileInputStream extends InputStream { + + public abstract void prepareExtractionForFileHeader(FileHeader fileHeader) throws IOException; +} diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/ZipEntryInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/ZipEntryInputStream.java index 9d92593..68d032a 100644 --- a/src/main/java/net/lingala/zip4j/io/inputstream/ZipEntryInputStream.java +++ b/src/main/java/net/lingala/zip4j/io/inputstream/ZipEntryInputStream.java @@ -58,6 +58,10 @@ public int readRawFully(byte[] b) throws IOException { int readLen = inputStream.read(b); + if (readLen == -1) { + throw new IOException("Unexpected EOF reached when trying to read stream"); + } + if (readLen != b.length) { readLen = readUntilBufferIsFull(b, readLen); diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/ZipInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/ZipInputStream.java index 52a55c3..cf3ca84 100644 --- a/src/main/java/net/lingala/zip4j/io/inputstream/ZipInputStream.java +++ b/src/main/java/net/lingala/zip4j/io/inputstream/ZipInputStream.java @@ -16,12 +16,10 @@ package net.lingala.zip4j.io.inputstream; -import static net.lingala.zip4j.util.InternalZipConstants.MIN_BUFF_SIZE; -import static net.lingala.zip4j.util.Zip4jUtil.getCompressionMethod; - import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.headers.HeaderReader; import net.lingala.zip4j.headers.HeaderSignature; +import net.lingala.zip4j.model.AESExtraDataRecord; import net.lingala.zip4j.model.DataDescriptor; import net.lingala.zip4j.model.ExtraDataRecord; import net.lingala.zip4j.model.FileHeader; @@ -40,6 +38,11 @@ import java.util.List; import java.util.zip.CRC32; +import static net.lingala.zip4j.util.InternalZipConstants.BUFF_SIZE; +import static net.lingala.zip4j.util.InternalZipConstants.MIN_BUFF_SIZE; +import static net.lingala.zip4j.util.InternalZipConstants.USE_UTF8_FOR_PASSWORD_ENCODING_DECODING; +import static net.lingala.zip4j.util.Zip4jUtil.getCompressionMethod; + public class ZipInputStream extends InputStream { private PushbackInputStream inputStream; @@ -72,11 +75,11 @@ public ZipInputStream(InputStream inputStream, PasswordCallback passwordCallback } public ZipInputStream(InputStream inputStream, char[] password, Charset charset) { - this(inputStream, password, new Zip4jConfig(charset, InternalZipConstants.BUFF_SIZE)); + this(inputStream, password, new Zip4jConfig(charset, BUFF_SIZE, USE_UTF8_FOR_PASSWORD_ENCODING_DECODING)); } public ZipInputStream(InputStream inputStream, PasswordCallback passwordCallback, Charset charset) { - this(inputStream, passwordCallback, new Zip4jConfig(charset, InternalZipConstants.BUFF_SIZE)); + this(inputStream, passwordCallback, new Zip4jConfig(charset, BUFF_SIZE, USE_UTF8_FOR_PASSWORD_ENCODING_DECODING)); } public ZipInputStream(InputStream inputStream, char[] password, Zip4jConfig zip4jConfig) { @@ -252,9 +255,11 @@ private CipherInputStream initializeCipherInputStream(ZipEntryInputStream zipEnt } if (localFileHeader.getEncryptionMethod() == EncryptionMethod.AES) { - return new AesCipherInputStream(zipEntryInputStream, localFileHeader, password, zip4jConfig.getBufferSize()); + return new AesCipherInputStream(zipEntryInputStream, localFileHeader, password, zip4jConfig.getBufferSize(), + zip4jConfig.isUseUtf8CharsetForPasswords()); } else if (localFileHeader.getEncryptionMethod() == EncryptionMethod.ZIP_STANDARD) { - return new ZipStandardCipherInputStream(zipEntryInputStream, localFileHeader, password, zip4jConfig.getBufferSize()); + return new ZipStandardCipherInputStream(zipEntryInputStream, localFileHeader, password, zip4jConfig.getBufferSize(), + zip4jConfig.isUseUtf8CharsetForPasswords()); } else { final String message = String.format("Entry [%s] Strong Encryption not supported", localFileHeader.getFileName()); throw new ZipException(message, ZipException.Type.UNSUPPORTED_ENCRYPTION); @@ -262,7 +267,7 @@ private CipherInputStream initializeCipherInputStream(ZipEntryInputStream zipEnt } private DecompressedInputStream initializeDecompressorForThisEntry(CipherInputStream cipherInputStream, - LocalFileHeader localFileHeader) { + LocalFileHeader localFileHeader) throws ZipException { CompressionMethod compressionMethod = getCompressionMethod(localFileHeader); if (compressionMethod == CompressionMethod.DEFLATE) { @@ -335,7 +340,7 @@ private boolean isEntryDirectory(String entryName) { return entryName.endsWith("/") || entryName.endsWith("\\"); } - private long getCompressedSize(LocalFileHeader localFileHeader) { + private long getCompressedSize(LocalFileHeader localFileHeader) throws ZipException { if (getCompressionMethod(localFileHeader).equals(CompressionMethod.STORE)) { return localFileHeader.getUncompressedSize(); } @@ -347,14 +352,13 @@ private long getCompressedSize(LocalFileHeader localFileHeader) { return localFileHeader.getCompressedSize() - getEncryptionHeaderSize(localFileHeader); } - private int getEncryptionHeaderSize(LocalFileHeader localFileHeader) { + private int getEncryptionHeaderSize(LocalFileHeader localFileHeader) throws ZipException { if (!localFileHeader.isEncrypted()) { return 0; } if (localFileHeader.getEncryptionMethod().equals(EncryptionMethod.AES)) { - return InternalZipConstants.AES_AUTH_LENGTH + InternalZipConstants.AES_PASSWORD_VERIFIER_LENGTH - + localFileHeader.getAesExtraDataRecord().getAesKeyStrength().getSaltLength(); + return getAesEncryptionHeaderSize(localFileHeader.getAesExtraDataRecord()); } else if (localFileHeader.getEncryptionMethod().equals(EncryptionMethod.ZIP_STANDARD)) { return InternalZipConstants.STD_DEC_HDR_SIZE; } else { @@ -363,11 +367,6 @@ private int getEncryptionHeaderSize(LocalFileHeader localFileHeader) { } private void readUntilEndOfEntry() throws IOException { - if ((localFileHeader.isDirectory() || localFileHeader.getCompressedSize() == 0) - && !localFileHeader.isDataDescriptorExists()) { - return; - } - if (endOfEntryBuffer == null) { endOfEntryBuffer = new byte[512]; } @@ -377,6 +376,15 @@ private void readUntilEndOfEntry() throws IOException { this.entryEOFReached = true; } + private int getAesEncryptionHeaderSize(AESExtraDataRecord aesExtraDataRecord) throws ZipException { + if (aesExtraDataRecord == null || aesExtraDataRecord.getAesKeyStrength() == null) { + throw new ZipException("AesExtraDataRecord not found or invalid for Aes encrypted entry"); + } + + return InternalZipConstants.AES_AUTH_LENGTH + InternalZipConstants.AES_PASSWORD_VERIFIER_LENGTH + + aesExtraDataRecord.getAesKeyStrength().getSaltLength(); + } + private boolean isEncryptionMethodZipStandard(LocalFileHeader localFileHeader) { return localFileHeader.isEncrypted() && EncryptionMethod.ZIP_STANDARD.equals(localFileHeader.getEncryptionMethod()); } diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/ZipStandardCipherInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/ZipStandardCipherInputStream.java index f110c76..f69afe3 100644 --- a/src/main/java/net/lingala/zip4j/io/inputstream/ZipStandardCipherInputStream.java +++ b/src/main/java/net/lingala/zip4j/io/inputstream/ZipStandardCipherInputStream.java @@ -10,14 +10,15 @@ class ZipStandardCipherInputStream extends CipherInputStream { public ZipStandardCipherInputStream(ZipEntryInputStream zipEntryInputStream, LocalFileHeader localFileHeader, - char[] password, int bufferSize) throws IOException { - super(zipEntryInputStream, localFileHeader, password, bufferSize); + char[] password, int bufferSize, boolean useUtf8ForPassword) throws IOException { + super(zipEntryInputStream, localFileHeader, password, bufferSize, useUtf8ForPassword); } @Override - protected StandardDecrypter initializeDecrypter(LocalFileHeader localFileHeader, char[] password) throws IOException { + protected StandardDecrypter initializeDecrypter(LocalFileHeader localFileHeader, char[] password, + boolean useUtf8ForPassword) throws IOException { return new StandardDecrypter(password, localFileHeader.getCrc(), localFileHeader.getLastModifiedTime(), - getStandardDecrypterHeaderBytes()); + getStandardDecrypterHeaderBytes(), useUtf8ForPassword); } private byte[] getStandardDecrypterHeaderBytes() throws IOException { diff --git a/src/main/java/net/lingala/zip4j/io/inputstream/ZipStandardSplitFileInputStream.java b/src/main/java/net/lingala/zip4j/io/inputstream/ZipStandardSplitFileInputStream.java new file mode 100644 index 0000000..2857d69 --- /dev/null +++ b/src/main/java/net/lingala/zip4j/io/inputstream/ZipStandardSplitFileInputStream.java @@ -0,0 +1,106 @@ +package net.lingala.zip4j.io.inputstream; + +import net.lingala.zip4j.model.FileHeader; +import net.lingala.zip4j.model.enums.RandomAccessFileMode; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * A split input stream for zip file split as per zip specification. They end with .z01, .z02... .zip + */ +public class ZipStandardSplitFileInputStream extends SplitFileInputStream { + + protected RandomAccessFile randomAccessFile; + protected File zipFile; + private int lastSplitZipFileNumber; + private boolean isSplitZipArchive; + private int currentSplitFileCounter = 0; + private byte[] singleByteArray = new byte[1]; + + public ZipStandardSplitFileInputStream(File zipFile, boolean isSplitZipArchive, int lastSplitZipFileNumber) throws FileNotFoundException { + this.randomAccessFile = new RandomAccessFile(zipFile, RandomAccessFileMode.READ.getValue()); + this.zipFile = zipFile; + this.isSplitZipArchive = isSplitZipArchive; + this.lastSplitZipFileNumber = lastSplitZipFileNumber; + + if (isSplitZipArchive) { + currentSplitFileCounter = lastSplitZipFileNumber; + } + } + + @Override + public int read() throws IOException { + int readLen = read(singleByteArray); + if (readLen == -1) { + return -1; + } + + return singleByteArray[0]; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int readLen = randomAccessFile.read(b, off, len); + + if ((readLen != len || readLen == -1) && isSplitZipArchive) { + openRandomAccessFileForIndex(currentSplitFileCounter + 1); + currentSplitFileCounter++; + + if (readLen < 0) readLen = 0; + int newlyRead = randomAccessFile.read(b, readLen, len - readLen); + if (newlyRead > 0) readLen += newlyRead; + } + + return readLen; + } + + @Override + public void prepareExtractionForFileHeader(FileHeader fileHeader) throws IOException { + + if (isSplitZipArchive && (currentSplitFileCounter != fileHeader.getDiskNumberStart())) { + openRandomAccessFileForIndex(fileHeader.getDiskNumberStart()); + currentSplitFileCounter = fileHeader.getDiskNumberStart(); + } + + randomAccessFile.seek(fileHeader.getOffsetLocalHeader()); + } + + @Override + public void close() throws IOException { + if (randomAccessFile != null) { + randomAccessFile.close(); + } + } + + protected void openRandomAccessFileForIndex(int zipFileIndex) throws IOException { + File nextSplitFile = getNextSplitFile(zipFileIndex); + if (!nextSplitFile.exists()) { + throw new FileNotFoundException("zip split file does not exist: " + nextSplitFile); + } + randomAccessFile.close(); + randomAccessFile = new RandomAccessFile(nextSplitFile, RandomAccessFileMode.READ.getValue()); + } + + protected File getNextSplitFile(int zipFileIndex) throws IOException { + if (zipFileIndex == lastSplitZipFileNumber) { + return zipFile; + } + + String currZipFileNameWithPath = zipFile.getCanonicalPath(); + String extensionSubString = ".z0"; + if (zipFileIndex >= 9) { + extensionSubString = ".z"; + } + + return new File(currZipFileNameWithPath.substring(0, + currZipFileNameWithPath.lastIndexOf(".")) + extensionSubString + (zipFileIndex + 1)); + } +} diff --git a/src/main/java/net/lingala/zip4j/io/outputstream/AesCipherOutputStream.java b/src/main/java/net/lingala/zip4j/io/outputstream/AesCipherOutputStream.java index 55ba5b9..51308ca 100644 --- a/src/main/java/net/lingala/zip4j/io/outputstream/AesCipherOutputStream.java +++ b/src/main/java/net/lingala/zip4j/io/outputstream/AesCipherOutputStream.java @@ -1,7 +1,6 @@ package net.lingala.zip4j.io.outputstream; import net.lingala.zip4j.crypto.AESEncrypter; -import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.model.ZipParameters; import java.io.IOException; @@ -14,13 +13,15 @@ class AesCipherOutputStream extends CipherOutputStream { private byte[] pendingBuffer = new byte[AES_BLOCK_SIZE]; private int pendingBufferLength = 0; - public AesCipherOutputStream(ZipEntryOutputStream outputStream, ZipParameters zipParameters, char[] password) throws IOException, ZipException { - super(outputStream, zipParameters, password); + public AesCipherOutputStream(ZipEntryOutputStream outputStream, ZipParameters zipParameters, char[] password, + boolean useUtf8ForPassword) throws IOException { + super(outputStream, zipParameters, password, useUtf8ForPassword); } @Override - protected AESEncrypter initializeEncrypter(OutputStream outputStream, ZipParameters zipParameters, char[] password) throws IOException, ZipException { - AESEncrypter encrypter = new AESEncrypter(password, zipParameters.getAesKeyStrength()); + protected AESEncrypter initializeEncrypter(OutputStream outputStream, ZipParameters zipParameters, char[] password, + boolean useUtf8ForPassword) throws IOException { + AESEncrypter encrypter = new AESEncrypter(password, zipParameters.getAesKeyStrength(), useUtf8ForPassword); writeAesEncryptionHeaderData(encrypter); return encrypter; } diff --git a/src/main/java/net/lingala/zip4j/io/outputstream/CipherOutputStream.java b/src/main/java/net/lingala/zip4j/io/outputstream/CipherOutputStream.java index d38279c..0760bd3 100644 --- a/src/main/java/net/lingala/zip4j/io/outputstream/CipherOutputStream.java +++ b/src/main/java/net/lingala/zip4j/io/outputstream/CipherOutputStream.java @@ -17,7 +17,6 @@ package net.lingala.zip4j.io.outputstream; import net.lingala.zip4j.crypto.Encrypter; -import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.model.ZipParameters; import java.io.IOException; @@ -28,10 +27,10 @@ abstract class CipherOutputStream extends OutputStream { private ZipEntryOutputStream zipEntryOutputStream; private T encrypter; - public CipherOutputStream(ZipEntryOutputStream zipEntryOutputStream, ZipParameters zipParameters, char[] password) - throws IOException, ZipException { + public CipherOutputStream(ZipEntryOutputStream zipEntryOutputStream, ZipParameters zipParameters, char[] password, + boolean useUtf8ForPassword) throws IOException { this.zipEntryOutputStream = zipEntryOutputStream; - this.encrypter = initializeEncrypter(zipEntryOutputStream, zipParameters, password); + this.encrypter = initializeEncrypter(zipEntryOutputStream, zipParameters, password, useUtf8ForPassword); } @Override @@ -71,6 +70,6 @@ protected T getEncrypter() { return encrypter; } - protected abstract T initializeEncrypter(OutputStream outputStream, ZipParameters zipParameters, char[] password) - throws IOException, ZipException; + protected abstract T initializeEncrypter(OutputStream outputStream, ZipParameters zipParameters, + char[] password, boolean useUtf8ForPassword) throws IOException; } diff --git a/src/main/java/net/lingala/zip4j/io/outputstream/NoCipherOutputStream.java b/src/main/java/net/lingala/zip4j/io/outputstream/NoCipherOutputStream.java index 3d2df96..ebbffdd 100644 --- a/src/main/java/net/lingala/zip4j/io/outputstream/NoCipherOutputStream.java +++ b/src/main/java/net/lingala/zip4j/io/outputstream/NoCipherOutputStream.java @@ -1,7 +1,6 @@ package net.lingala.zip4j.io.outputstream; import net.lingala.zip4j.crypto.Encrypter; -import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.model.ZipParameters; import java.io.IOException; @@ -9,12 +8,14 @@ class NoCipherOutputStream extends CipherOutputStream { - public NoCipherOutputStream(ZipEntryOutputStream zipEntryOutputStream, ZipParameters zipParameters, char[] password) throws IOException, ZipException { - super(zipEntryOutputStream, zipParameters, password); + public NoCipherOutputStream(ZipEntryOutputStream zipEntryOutputStream, ZipParameters zipParameters, char[] password) + throws IOException { + super(zipEntryOutputStream, zipParameters, password, true); } @Override - protected NoEncrypter initializeEncrypter(OutputStream outputStream, ZipParameters zipParameters, char[] password) { + protected NoEncrypter initializeEncrypter(OutputStream outputStream, ZipParameters zipParameters, char[] password, + boolean useUtf8ForPassword) { return new NoEncrypter(); } diff --git a/src/main/java/net/lingala/zip4j/io/outputstream/ZipOutputStream.java b/src/main/java/net/lingala/zip4j/io/outputstream/ZipOutputStream.java index a20beb0..3840440 100644 --- a/src/main/java/net/lingala/zip4j/io/outputstream/ZipOutputStream.java +++ b/src/main/java/net/lingala/zip4j/io/outputstream/ZipOutputStream.java @@ -14,6 +14,7 @@ import net.lingala.zip4j.model.enums.EncryptionMethod; import net.lingala.zip4j.util.InternalZipConstants; import net.lingala.zip4j.util.RawIO; +import net.lingala.zip4j.util.Zip4jUtil; import java.io.IOException; import java.io.OutputStream; @@ -23,6 +24,7 @@ import static net.lingala.zip4j.util.FileUtils.isZipEntryDirectory; import static net.lingala.zip4j.util.InternalZipConstants.BUFF_SIZE; import static net.lingala.zip4j.util.InternalZipConstants.MIN_BUFF_SIZE; +import static net.lingala.zip4j.util.InternalZipConstants.USE_UTF8_FOR_PASSWORD_ENCODING_DECODING; public class ZipOutputStream extends OutputStream { @@ -54,7 +56,10 @@ public ZipOutputStream(OutputStream outputStream, char[] password) throws IOExce } public ZipOutputStream(OutputStream outputStream, char[] password, Charset charset) throws IOException { - this(outputStream, password, new Zip4jConfig(charset, BUFF_SIZE), new ZipModel()); + this(outputStream, + password, + new Zip4jConfig(charset, BUFF_SIZE, USE_UTF8_FOR_PASSWORD_ENCODING_DECODING), + new ZipModel()); } public ZipOutputStream(OutputStream outputStream, char[] password, Zip4jConfig zip4jConfig, @@ -79,6 +84,11 @@ public void putNextEntry(ZipParameters zipParameters) throws IOException { clonedZipParameters.setWriteExtendedLocalFileHeader(false); clonedZipParameters.setCompressionMethod(CompressionMethod.STORE); clonedZipParameters.setEncryptFiles(false); + clonedZipParameters.setEntrySize(0); + + if (zipParameters.getLastModifiedFileTime() <= 0) { + clonedZipParameters.setLastModifiedFileTime(System.currentTimeMillis()); + } } initializeAndWriteFileHeader(clonedZipParameters); @@ -206,9 +216,9 @@ private CipherOutputStream initializeCipherOutputStream(ZipEntryOutputStream zip } if (zipParameters.getEncryptionMethod() == EncryptionMethod.AES) { - return new AesCipherOutputStream(zipEntryOutputStream, zipParameters, password); + return new AesCipherOutputStream(zipEntryOutputStream, zipParameters, password, zip4jConfig.isUseUtf8CharsetForPasswords()); } else if (zipParameters.getEncryptionMethod() == EncryptionMethod.ZIP_STANDARD) { - return new ZipStandardCipherOutputStream(zipEntryOutputStream, zipParameters, password); + return new ZipStandardCipherOutputStream(zipEntryOutputStream, zipParameters, password, zip4jConfig.isUseUtf8CharsetForPasswords()); } else if (zipParameters.getEncryptionMethod() == EncryptionMethod.ZIP_STANDARD_VARIANT_STRONG) { throw new ZipException(EncryptionMethod.ZIP_STANDARD_VARIANT_STRONG + " encryption method is not supported"); } else { @@ -226,6 +236,10 @@ private CompressedOutputStream initializeCompressedOutputStream(CipherOutputStre } private void verifyZipParameters(ZipParameters zipParameters) { + if (Zip4jUtil.isStringNullOrEmpty(zipParameters.getFileNameInZip())) { + throw new IllegalArgumentException("fileNameInZip is null or empty"); + } + if (zipParameters.getCompressionMethod() == CompressionMethod.STORE && zipParameters.getEntrySize() < 0 && !isZipEntryDirectory(zipParameters.getFileNameInZip()) diff --git a/src/main/java/net/lingala/zip4j/io/outputstream/ZipStandardCipherOutputStream.java b/src/main/java/net/lingala/zip4j/io/outputstream/ZipStandardCipherOutputStream.java index 4bdcc8e..442642e 100644 --- a/src/main/java/net/lingala/zip4j/io/outputstream/ZipStandardCipherOutputStream.java +++ b/src/main/java/net/lingala/zip4j/io/outputstream/ZipStandardCipherOutputStream.java @@ -9,16 +9,16 @@ class ZipStandardCipherOutputStream extends CipherOutputStream { - public ZipStandardCipherOutputStream(ZipEntryOutputStream outputStream, ZipParameters zipParameters, char[] password) - throws IOException { - super(outputStream, zipParameters, password); + public ZipStandardCipherOutputStream(ZipEntryOutputStream outputStream, ZipParameters zipParameters, char[] password, + boolean useUtf8ForPassword) throws IOException { + super(outputStream, zipParameters, password, useUtf8ForPassword); } @Override protected StandardEncrypter initializeEncrypter(OutputStream outputStream, ZipParameters zipParameters, - char[] password) throws IOException { + char[] password, boolean useUtf8ForPassword) throws IOException { long key = getEncryptionKey(zipParameters); - StandardEncrypter encrypter = new StandardEncrypter(password, key); + StandardEncrypter encrypter = new StandardEncrypter(password, key, useUtf8ForPassword); writeHeaders(encrypter.getHeaderBytes()); return encrypter; } diff --git a/src/main/java/net/lingala/zip4j/model/Zip4jConfig.java b/src/main/java/net/lingala/zip4j/model/Zip4jConfig.java index 5ba3c3b..f5fb3d7 100644 --- a/src/main/java/net/lingala/zip4j/model/Zip4jConfig.java +++ b/src/main/java/net/lingala/zip4j/model/Zip4jConfig.java @@ -6,10 +6,12 @@ public class Zip4jConfig { private final Charset charset; private final int bufferSize; + private final boolean useUtf8CharsetForPasswords; - public Zip4jConfig(Charset charset, int bufferSize) { + public Zip4jConfig(Charset charset, int bufferSize, boolean useUtf8CharsetForPasswords) { this.charset = charset; this.bufferSize = bufferSize; + this.useUtf8CharsetForPasswords = useUtf8CharsetForPasswords; } public Charset getCharset() { @@ -19,4 +21,8 @@ public Charset getCharset() { public int getBufferSize() { return bufferSize; } + + public boolean isUseUtf8CharsetForPasswords() { + return useUtf8CharsetForPasswords; + } } diff --git a/src/main/java/net/lingala/zip4j/model/ZipParameters.java b/src/main/java/net/lingala/zip4j/model/ZipParameters.java index 350f1b3..06fdd3d 100644 --- a/src/main/java/net/lingala/zip4j/model/ZipParameters.java +++ b/src/main/java/net/lingala/zip4j/model/ZipParameters.java @@ -34,11 +34,11 @@ public enum SymbolicLinkAction { /** * Add only the symbolic link itself, not the target file or its contents */ - INCLUDE_LINK_ONLY, + INCLUDE_LINK_ONLY, /** * Add only the target file and its contents, using the filename of the symbolic link */ - INCLUDE_LINKED_FILE_ONLY, + INCLUDE_LINKED_FILE_ONLY, /** * Add the symbolic link itself and the target file with its original filename and its contents */ @@ -57,7 +57,7 @@ public enum SymbolicLinkAction { private long entryCRC; private String defaultFolderPath; private String fileNameInZip; - private long lastModifiedFileTime = System.currentTimeMillis(); + private long lastModifiedFileTime = 0; private long entrySize = -1; private boolean writeExtendedLocalFileHeader = true; private boolean overrideExistingFilesInZip = true; @@ -72,7 +72,7 @@ public enum SymbolicLinkAction { * CompressionMethod.DEFLATE, CompressionLevel.NORMAL, EncryptionMethod.NONE, * AesKeyStrength.KEY_STRENGTH_256, AesVerson.Two, SymbolicLinkAction.INCLUDE_LINKED_FILE_ONLY, * readHiddenFiles is true, readHiddenFolders is true, includeRootInFolder is true, - * writeExtendedLocalFileHeader is true, overrideExistingFilesInZip is true + * writeExtendedLocalFileHeader is true, overrideExistingFilesInZip is true */ public ZipParameters() { } @@ -113,7 +113,7 @@ public CompressionMethod getCompressionMethod() { return compressionMethod; } - /** + /** * Set the ZIP compression method * @param compressionMethod the ZIP compression method */ @@ -171,31 +171,31 @@ public void setCompressionLevel(CompressionLevel compressionLevel) { /** * Test if hidden files will be included during folder recursion - * + * * @return true if hidden files will be included when adding folders to the zip */ public boolean isReadHiddenFiles() { return readHiddenFiles; } - + /** * Indicate if hidden files will be included during folder recursion - * + * * @param readHiddenFiles if true, hidden files will be included when adding folders to the zip */ public void setReadHiddenFiles(boolean readHiddenFiles) { this.readHiddenFiles = readHiddenFiles; } - + /** * Test if hidden folders will be included during folder recursion - * + * * @return true if hidden folders will be included when adding folders to the zip */ public boolean isReadHiddenFolders() { return readHiddenFolders; } - + /** * Indicate if hidden folders will be included during folder recursion * @param readHiddenFolders if true, hidden folders will be included when added folders to the zip @@ -204,10 +204,6 @@ public void setReadHiddenFolders(boolean readHiddenFolders) { this.readHiddenFolders = readHiddenFolders; } - public Object clone() throws CloneNotSupportedException { - return super.clone(); - } - /** * Get the key strength of the AES encryption key * @return the key strength of the AES encryption key @@ -217,7 +213,7 @@ public AesKeyStrength getAesKeyStrength() { } /** - * Set the key strength of the AES encryption key + * Set the key strength of the AES encryption key * @param aesKeyStrength the key strength of the AES encryption key */ public void setAesKeyStrength(AesKeyStrength aesKeyStrength) { @@ -279,10 +275,10 @@ public String getFileNameInZip() { /** * Set the filename that will be used to include a file into the ZIP file to a different name * that given by the source filename added to the ZIP file. The filenameInZip must - * adhere to the ZIP filename specification, including the use of forward slash '/' as the - * directory separator, and it must also be a relative file. If the filenameInZip given is not null and - * not empty, the value specified by setRootFolderNameInZip() will be ignored. - * + * adhere to the ZIP filename specification, including the use of forward slash '/' as the + * directory separator, and it must also be a relative file. If the filenameInZip given is not null and + * not empty, the value specified by setRootFolderNameInZip() will be ignored. + * * @param fileNameInZip the filename to set in the ZIP. Use null or an empty String to set the default behavior */ public void setFileNameInZip(String fileNameInZip) { @@ -290,7 +286,7 @@ public void setFileNameInZip(String fileNameInZip) { } /** - * Get the last modified time to be used for files written to the ZIP + * Get the last modified time to be used for files written to the ZIP * @return the last modified time in milliseconds since the epoch */ public long getLastModifiedFileTime() { @@ -345,7 +341,7 @@ public String getRootFolderNameInZip() { /** * Set the folder name that will be prepended to the filename in the ZIP. This value is ignored * if setFileNameInZip() is specified with a non-null, non-empty string. - * + * * @param rootFolderNameInZip the name of the folder to be prepended to the filename * in the ZIP archive */ diff --git a/src/main/java/net/lingala/zip4j/model/enums/AesVersion.java b/src/main/java/net/lingala/zip4j/model/enums/AesVersion.java index cf16f76..3fb99fe 100644 --- a/src/main/java/net/lingala/zip4j/model/enums/AesVersion.java +++ b/src/main/java/net/lingala/zip4j/model/enums/AesVersion.java @@ -1,16 +1,18 @@ package net.lingala.zip4j.model.enums; +import net.lingala.zip4j.exception.ZipException; + /** * Indicates the AES format used */ public enum AesVersion { /** - * Version 1 of the AES format + * Version 1 of the AES format */ ONE(1), /** - * Version 2 of the AES format + * Version 2 of the AES format */ TWO(2); @@ -32,13 +34,13 @@ public int getVersionNumber() { * @return the AESVersion instance for a given version * @throws IllegalArgumentException if an unsupported version is given */ - public static AesVersion getFromVersionNumber(int versionNumber) { + public static AesVersion getFromVersionNumber(int versionNumber) throws ZipException { for (AesVersion aesVersion : values()) { if (aesVersion.versionNumber == versionNumber) { return aesVersion; } } - throw new IllegalArgumentException("Unsupported Aes version"); + throw new ZipException("Unsupported Aes version"); } } diff --git a/src/main/java/net/lingala/zip4j/model/enums/CompressionLevel.java b/src/main/java/net/lingala/zip4j/model/enums/CompressionLevel.java index d54ef77..a57da8f 100644 --- a/src/main/java/net/lingala/zip4j/model/enums/CompressionLevel.java +++ b/src/main/java/net/lingala/zip4j/model/enums/CompressionLevel.java @@ -6,6 +6,10 @@ */ public enum CompressionLevel { + /** + * Level 0 - No compression + */ + NO_COMPRESSION(0), /** * Level 1 Deflate compression. Fastest compression. */ @@ -43,7 +47,7 @@ public enum CompressionLevel { */ ULTRA(9); - private int level; + private final int level; CompressionLevel(int level) { this.level = level; diff --git a/src/main/java/net/lingala/zip4j/tasks/AbstractAddFileToZipTask.java b/src/main/java/net/lingala/zip4j/tasks/AbstractAddFileToZipTask.java index 6bce374..03d5ffc 100644 --- a/src/main/java/net/lingala/zip4j/tasks/AbstractAddFileToZipTask.java +++ b/src/main/java/net/lingala/zip4j/tasks/AbstractAddFileToZipTask.java @@ -39,7 +39,6 @@ import static net.lingala.zip4j.util.CrcUtil.computeFileCrc; import static net.lingala.zip4j.util.FileUtils.assertFilesExist; import static net.lingala.zip4j.util.FileUtils.getRelativeFileName; -import static net.lingala.zip4j.util.Zip4jUtil.epochToExtendedDosTime; public abstract class AbstractAddFileToZipTask extends AsyncZipTask { @@ -197,10 +196,11 @@ void updateLocalFileHeader(FileHeader fileHeader, SplitOutputStream splitOutputS headerWriter.updateLocalFileHeader(fileHeader, getZipModel(), splitOutputStream); } + // Suppressing warning to use BasicFileAttributes as this has trouble reading symlink's attributes + @SuppressWarnings("BulkFileAttributesRead") private ZipParameters cloneAndAdjustZipParameters(ZipParameters zipParameters, File fileToAdd, ProgressMonitor progressMonitor) throws IOException { ZipParameters clonedZipParameters = new ZipParameters(zipParameters); - clonedZipParameters.setLastModifiedFileTime(epochToExtendedDosTime((fileToAdd.lastModified()))); if (fileToAdd.isDirectory()) { clonedZipParameters.setEntrySize(0); @@ -208,8 +208,11 @@ private ZipParameters cloneAndAdjustZipParameters(ZipParameters zipParameters, F clonedZipParameters.setEntrySize(fileToAdd.length()); } + if (zipParameters.getLastModifiedFileTime() <= 0) { + clonedZipParameters.setLastModifiedFileTime(fileToAdd.lastModified()); + } + clonedZipParameters.setWriteExtendedLocalFileHeader(false); - clonedZipParameters.setLastModifiedFileTime(fileToAdd.lastModified()); if (!Zip4jUtil.isStringNotNullAndNotEmpty(zipParameters.getFileNameInZip())) { String relativeFileName = getRelativeFileName(fileToAdd, zipParameters); @@ -217,8 +220,8 @@ private ZipParameters cloneAndAdjustZipParameters(ZipParameters zipParameters, F } if (fileToAdd.isDirectory()) { - clonedZipParameters.setCompressionMethod(CompressionMethod.STORE); - clonedZipParameters.setEncryptionMethod(EncryptionMethod.NONE); + clonedZipParameters.setCompressionMethod(STORE); + clonedZipParameters.setEncryptionMethod(NONE); clonedZipParameters.setEncryptFiles(false); } else { if (clonedZipParameters.isEncryptFiles() && clonedZipParameters.getEncryptionMethod() == ZIP_STANDARD) { @@ -228,7 +231,7 @@ private ZipParameters cloneAndAdjustZipParameters(ZipParameters zipParameters, F } if (fileToAdd.length() == 0) { - clonedZipParameters.setCompressionMethod(CompressionMethod.STORE); + clonedZipParameters.setCompressionMethod(STORE); } } diff --git a/src/main/java/net/lingala/zip4j/tasks/AbstractExtractFileTask.java b/src/main/java/net/lingala/zip4j/tasks/AbstractExtractFileTask.java index dd6113e..0169ef4 100644 --- a/src/main/java/net/lingala/zip4j/tasks/AbstractExtractFileTask.java +++ b/src/main/java/net/lingala/zip4j/tasks/AbstractExtractFileTask.java @@ -18,6 +18,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.regex.Matcher; import static net.lingala.zip4j.util.InternalZipConstants.FILE_SEPARATOR; @@ -48,12 +49,7 @@ protected void extractFile(ZipInputStream zipInputStream, FileHeader fileHeader, File outputFile = determineOutputFile(fileHeader, outputPath, newFileName); progressMonitor.setFileName(outputFile.getAbsolutePath()); - // make sure no file is extracted outside of the target directory (a.k.a zip slip) - String outputCanonicalPath = (new File(outputPath).getCanonicalPath()) + File.separator; - if (!outputFile.getCanonicalPath().startsWith(outputCanonicalPath)) { - throw new ZipException("illegal file name that breaks out of the target directory: " - + fileHeader.getFileName()); - } + assertCanonicalPathsAreSame(outputFile, outputPath, fileHeader); verifyNextEntry(zipInputStream, fileHeader); @@ -73,6 +69,22 @@ protected void extractFile(ZipInputStream zipInputStream, FileHeader fileHeader, UnzipUtil.applyFileAttributes(fileHeader, outputFile); } + private void assertCanonicalPathsAreSame(File outputFile, String outputPath, FileHeader fileHeader) + throws IOException { + + String outputFileCanonicalPath = outputFile.getCanonicalPath(); + if (outputFile.isDirectory() && !outputFileCanonicalPath.endsWith(FILE_SEPARATOR)) { + outputFileCanonicalPath = outputFileCanonicalPath + FILE_SEPARATOR; + } + + // make sure no file is extracted outside the target directory (a.k.a. zip slip) + String outputCanonicalPath = (new File(outputPath).getCanonicalPath()) + File.separator; + if (!outputFileCanonicalPath.startsWith(outputCanonicalPath)) { + throw new ZipException("illegal file name that breaks out of the target directory: " + + fileHeader.getFileName()); + } + } + private boolean isSymbolicLink(FileHeader fileHeader) { byte[] externalFileAttributes = fileHeader.getExternalFileAttributes(); @@ -162,7 +174,12 @@ private File determineOutputFile(FileHeader fileHeader, String outputPath, Strin if (Zip4jUtil.isStringNotNullAndNotEmpty(newFileName)) { outputFileName = newFileName; } - return new File(outputPath + FILE_SEPARATOR + outputFileName); + return new File(outputPath, getFileNameWithSystemFileSeparators(outputFileName)); + } + + private String getFileNameWithSystemFileSeparators(String fileNameToReplace) { + String formattedFileName = fileNameToReplace.replaceAll(":\\\\", "_"); + return formattedFileName.replaceAll("[/\\\\]", Matcher.quoteReplacement(FILE_SEPARATOR)); } @Override diff --git a/src/main/java/net/lingala/zip4j/tasks/AddFilesToZipTask.java b/src/main/java/net/lingala/zip4j/tasks/AddFilesToZipTask.java index 2813ab7..718343e 100644 --- a/src/main/java/net/lingala/zip4j/tasks/AddFilesToZipTask.java +++ b/src/main/java/net/lingala/zip4j/tasks/AddFilesToZipTask.java @@ -7,11 +7,16 @@ import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.progress.ProgressMonitor; import net.lingala.zip4j.tasks.AddFilesToZipTask.AddFilesToZipTaskParameters; +import net.lingala.zip4j.util.FileUtils; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import static net.lingala.zip4j.model.ZipParameters.SymbolicLinkAction.INCLUDE_LINK_ONLY; +import static net.lingala.zip4j.util.FileUtils.isSymbolicLink; + public class AddFilesToZipTask extends AbstractAddFileToZipTask { public AddFilesToZipTask(ZipModel zipModel, char[] password, HeaderWriter headerWriter, @@ -24,7 +29,8 @@ protected void executeTask(AddFilesToZipTaskParameters taskParameters, ProgressM throws IOException { verifyZipParameters(taskParameters.zipParameters); - addFilesToZip(taskParameters.filesToAdd, progressMonitor, taskParameters.zipParameters, taskParameters.zip4jConfig); + List filesToAdd = determineActualFilesToAdd(taskParameters); + addFilesToZip(filesToAdd, progressMonitor, taskParameters.zipParameters, taskParameters.zip4jConfig); } @Override @@ -32,6 +38,22 @@ protected long calculateTotalWork(AddFilesToZipTaskParameters taskParameters) th return calculateWorkForFiles(taskParameters.filesToAdd, taskParameters.zipParameters); } + private List determineActualFilesToAdd(AddFilesToZipTaskParameters taskParameters) throws ZipException { + List filesToAdd = new ArrayList<>(); + + for (File inputFile : taskParameters.filesToAdd) { + filesToAdd.add(inputFile); + boolean isSymLink = isSymbolicLink(inputFile); + ZipParameters.SymbolicLinkAction symbolicLinkAction = taskParameters.zipParameters.getSymbolicLinkAction(); + if (isSymLink && !INCLUDE_LINK_ONLY.equals(symbolicLinkAction)) { + filesToAdd.addAll(FileUtils.getFilesInDirectoryRecursive(inputFile, taskParameters.zipParameters)); + } + + } + + return filesToAdd; + } + @Override protected ProgressMonitor.Task getTask() { return super.getTask(); diff --git a/src/main/java/net/lingala/zip4j/tasks/AddFolderToZipTask.java b/src/main/java/net/lingala/zip4j/tasks/AddFolderToZipTask.java index a84f301..b48a2ef 100644 --- a/src/main/java/net/lingala/zip4j/tasks/AddFolderToZipTask.java +++ b/src/main/java/net/lingala/zip4j/tasks/AddFolderToZipTask.java @@ -30,10 +30,7 @@ protected void executeTask(AddFolderToZipTaskParameters taskParameters, Progress @Override protected long calculateTotalWork(AddFolderToZipTaskParameters taskParameters) throws ZipException { - List filesToAdd = getFilesInDirectoryRecursive(taskParameters.folderToAdd, - taskParameters.zipParameters.isReadHiddenFiles(), - taskParameters.zipParameters.isReadHiddenFolders(), - taskParameters.zipParameters.getExcludeFileFilter()); + List filesToAdd = getFilesToAdd(taskParameters); if (taskParameters.zipParameters.isIncludeRootFolder()) { filesToAdd.add(taskParameters.folderToAdd); @@ -60,10 +57,7 @@ private void setDefaultFolderPath(AddFolderToZipTaskParameters taskParameters) t } private List getFilesToAdd(AddFolderToZipTaskParameters taskParameters) throws ZipException { - List filesToAdd = getFilesInDirectoryRecursive(taskParameters.folderToAdd, - taskParameters.zipParameters.isReadHiddenFiles(), - taskParameters.zipParameters.isReadHiddenFolders(), - taskParameters.zipParameters.getExcludeFileFilter()); + List filesToAdd = getFilesInDirectoryRecursive(taskParameters.folderToAdd, taskParameters.zipParameters); if (taskParameters.zipParameters.isIncludeRootFolder()) { filesToAdd.add(taskParameters.folderToAdd); diff --git a/src/main/java/net/lingala/zip4j/tasks/ExtractAllFilesTask.java b/src/main/java/net/lingala/zip4j/tasks/ExtractAllFilesTask.java index 5766c89..f57d518 100644 --- a/src/main/java/net/lingala/zip4j/tasks/ExtractAllFilesTask.java +++ b/src/main/java/net/lingala/zip4j/tasks/ExtractAllFilesTask.java @@ -1,6 +1,6 @@ package net.lingala.zip4j.tasks; -import net.lingala.zip4j.io.inputstream.SplitInputStream; +import net.lingala.zip4j.io.inputstream.SplitFileInputStream; import net.lingala.zip4j.io.inputstream.ZipInputStream; import net.lingala.zip4j.model.FileHeader; import net.lingala.zip4j.model.UnzipParameters; @@ -17,7 +17,7 @@ public class ExtractAllFilesTask extends AbstractExtractFileTask { private final char[] password; - private SplitInputStream splitInputStream; + private SplitFileInputStream splitInputStream; public ExtractAllFilesTask(ZipModel zipModel, char[] password, UnzipParameters unzipParameters, AsyncTaskParameters asyncTaskParameters) { diff --git a/src/main/java/net/lingala/zip4j/tasks/ExtractFileTask.java b/src/main/java/net/lingala/zip4j/tasks/ExtractFileTask.java index 1c1dbba..700d7ad 100644 --- a/src/main/java/net/lingala/zip4j/tasks/ExtractFileTask.java +++ b/src/main/java/net/lingala/zip4j/tasks/ExtractFileTask.java @@ -2,7 +2,7 @@ import net.lingala.zip4j.exception.ZipException; import net.lingala.zip4j.headers.HeaderUtil; -import net.lingala.zip4j.io.inputstream.SplitInputStream; +import net.lingala.zip4j.io.inputstream.SplitFileInputStream; import net.lingala.zip4j.io.inputstream.ZipInputStream; import net.lingala.zip4j.model.FileHeader; import net.lingala.zip4j.model.UnzipParameters; @@ -26,7 +26,7 @@ public class ExtractFileTask extends AbstractExtractFileTask { private char[] password; - private SplitInputStream splitInputStream; + private SplitFileInputStream splitInputStream; public ExtractFileTask(ZipModel zipModel, char[] password, UnzipParameters unzipParameters, AsyncTaskParameters asyncTaskParameters) { diff --git a/src/main/java/net/lingala/zip4j/util/FileUtils.java b/src/main/java/net/lingala/zip4j/util/FileUtils.java index 5157a6b..b6206c4 100644 --- a/src/main/java/net/lingala/zip4j/util/FileUtils.java +++ b/src/main/java/net/lingala/zip4j/util/FileUtils.java @@ -1,7 +1,6 @@ package net.lingala.zip4j.util; import net.lingala.zip4j.exception.ZipException; -import net.lingala.zip4j.model.ExcludeFileFilter; import net.lingala.zip4j.model.ZipModel; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.progress.ProgressMonitor; @@ -34,6 +33,9 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; +import static net.lingala.zip4j.model.ZipParameters.SymbolicLinkAction.INCLUDE_LINKED_FILE_ONLY; +import static net.lingala.zip4j.model.ZipParameters.SymbolicLinkAction.INCLUDE_LINK_AND_LINKED_FILE; +import static net.lingala.zip4j.model.ZipParameters.SymbolicLinkAction.INCLUDE_LINK_ONLY; import static net.lingala.zip4j.util.BitUtils.isBitSet; import static net.lingala.zip4j.util.BitUtils.setBit; import static net.lingala.zip4j.util.InternalZipConstants.FILE_SEPARATOR; @@ -93,12 +95,8 @@ public static byte[] getFileAttributes(File file) { } } - public static List getFilesInDirectoryRecursive(File path, boolean readHiddenFiles, boolean readHiddenFolders) throws ZipException { - return getFilesInDirectoryRecursive(path, readHiddenFiles, readHiddenFolders, null); - } - - public static List getFilesInDirectoryRecursive(File path, boolean readHiddenFiles, boolean readHiddenFolders, ExcludeFileFilter excludedFiles) - throws ZipException { + public static List getFilesInDirectoryRecursive(File path, ZipParameters zipParameters) + throws ZipException { if (path == null) { throw new ZipException("input path is null, cannot read files in the directory"); @@ -112,22 +110,23 @@ public static List getFilesInDirectoryRecursive(File path, boolean readHid } for (File file : filesAndDirs) { - if (excludedFiles != null && excludedFiles.isExcluded(file)) { + if (zipParameters.getExcludeFileFilter() != null && zipParameters.getExcludeFileFilter().isExcluded(file)) { continue; } - if (file.isHidden()) { - if (file.isDirectory()) { - if (!readHiddenFolders) { - continue; - } - } else if (!readHiddenFiles) { - continue; - } + if (file.isHidden() && !zipParameters.isReadHiddenFiles()) { + continue; } + result.add(file); - if (file.isDirectory()) { - result.addAll(getFilesInDirectoryRecursive(file, readHiddenFiles, readHiddenFolders, excludedFiles)); + + ZipParameters.SymbolicLinkAction symbolicLinkAction = zipParameters.getSymbolicLinkAction(); + boolean isSymLink = isSymbolicLink(file); + // If a symlink's target is a directory, file.isDirectory is true. Only check if file is a directory is file is + // not a symlink. + if ((isSymLink && !INCLUDE_LINK_ONLY.equals(symbolicLinkAction)) + || (!isSymLink && file.isDirectory())) { + result.addAll(getFilesInDirectoryRecursive(file,zipParameters)); } } @@ -223,7 +222,11 @@ public static String getRelativeFileName(File fileToAdd, ZipParameters zipParame String rootPath = new File(fileToAdd.getParentFile().getCanonicalFile().getPath() + File.separator + fileToAdd.getCanonicalFile().getName()).getPath(); tmpFileName = rootPath.substring(rootFolderFileRef.length()); } else { - tmpFileName = fileCanonicalPath.substring(rootFolderFileRef.length()); + if (!fileCanonicalPath.startsWith(rootFolderFileRef)) { + tmpFileName = fileToAdd.getCanonicalFile().getParentFile().getName() + FILE_SEPARATOR + fileToAdd.getCanonicalFile().getName(); + } else { + tmpFileName = fileCanonicalPath.substring(rootFolderFileRef.length()); + } } if (tmpFileName.startsWith(System.getProperty("file.separator"))) { @@ -351,8 +354,8 @@ public static void assertFilesExist(List files, ZipParameters.SymbolicLink if (isSymbolicLink(file)) { // If symlink is INCLUDE_LINK_ONLY, and if the above condition is true, it means that the link exists and there // will be no need to check for link existence explicitly, check only for target file existence if required - if (symLinkAction.equals(ZipParameters.SymbolicLinkAction.INCLUDE_LINK_AND_LINKED_FILE) - || symLinkAction.equals(ZipParameters.SymbolicLinkAction.INCLUDE_LINKED_FILE_ONLY)) { + if (symLinkAction.equals(INCLUDE_LINK_AND_LINKED_FILE) + || symLinkAction.equals(INCLUDE_LINKED_FILE_ONLY)) { assertSymbolicLinkTargetExists(file); } } else { @@ -465,6 +468,15 @@ private static void applyWindowsFileAttributes(Path file, byte[] fileAttributes) } DosFileAttributeView fileAttributeView = Files.getFileAttributeView(file, DosFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + + //IntelliJ complains that fileAttributes can never be null. But apparently it can. + //See https://github.com/srikanth-lingala/zip4j/issues/435 + //Even the javadoc of Files.getFileAttributeView says it can be null + //noinspection ConstantConditions + if (fileAttributes == null) { + return; + } + try { fileAttributeView.setReadOnly(isBitSet(fileAttributes[0], 0)); fileAttributeView.setHidden(isBitSet(fileAttributes[0], 1)); @@ -504,6 +516,11 @@ private static byte[] getWindowsFileAttributes(Path file) { try { DosFileAttributeView dosFileAttributeView = Files.getFileAttributeView(file, DosFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + + if (dosFileAttributeView == null) { + return fileAttributes; + } + DosFileAttributes dosFileAttributes = dosFileAttributeView.readAttributes(); byte windowsAttribute = 0; diff --git a/src/main/java/net/lingala/zip4j/util/InternalZipConstants.java b/src/main/java/net/lingala/zip4j/util/InternalZipConstants.java index f619b92..7b09c14 100644 --- a/src/main/java/net/lingala/zip4j/util/InternalZipConstants.java +++ b/src/main/java/net/lingala/zip4j/util/InternalZipConstants.java @@ -68,4 +68,6 @@ private InternalZipConstants() { public static final Charset ZIP4J_DEFAULT_CHARSET = CHARSET_UTF_8; public static final String SEVEN_ZIP_SPLIT_FILE_EXTENSION_PATTERN = ".zip.001"; + + public static final boolean USE_UTF8_FOR_PASSWORD_ENCODING_DECODING = true; } diff --git a/src/main/java/net/lingala/zip4j/util/RawIO.java b/src/main/java/net/lingala/zip4j/util/RawIO.java index e5b4e3b..bf22bb9 100644 --- a/src/main/java/net/lingala/zip4j/util/RawIO.java +++ b/src/main/java/net/lingala/zip4j/util/RawIO.java @@ -56,7 +56,7 @@ public long readLongLittleEndian(byte[] array, int pos) { if (array.length - pos < 8) { resetBytes(longBuff); } - System.arraycopy(array, pos, longBuff, 0, array.length < 8 ? array.length - pos : 8); + System.arraycopy(array, pos, longBuff, 0, Math.min(array.length - pos, 8)); long temp = 0; temp |= longBuff[7] & 0xff; diff --git a/src/main/java/net/lingala/zip4j/util/UnzipUtil.java b/src/main/java/net/lingala/zip4j/util/UnzipUtil.java index 0238252..343ab3f 100644 --- a/src/main/java/net/lingala/zip4j/util/UnzipUtil.java +++ b/src/main/java/net/lingala/zip4j/util/UnzipUtil.java @@ -1,10 +1,10 @@ package net.lingala.zip4j.util; import net.lingala.zip4j.exception.ZipException; -import net.lingala.zip4j.io.inputstream.NumberedSplitInputStream; -import net.lingala.zip4j.io.inputstream.SplitInputStream; +import net.lingala.zip4j.io.inputstream.NumberedSplitFileInputStream; +import net.lingala.zip4j.io.inputstream.SplitFileInputStream; import net.lingala.zip4j.io.inputstream.ZipInputStream; -import net.lingala.zip4j.io.inputstream.ZipStandardSplitInputStream; +import net.lingala.zip4j.io.inputstream.ZipStandardSplitFileInputStream; import net.lingala.zip4j.model.FileHeader; import net.lingala.zip4j.model.ZipModel; @@ -21,7 +21,7 @@ public class UnzipUtil { public static ZipInputStream createZipInputStream(ZipModel zipModel, FileHeader fileHeader, char[] password) throws IOException { - SplitInputStream splitInputStream = null; + SplitFileInputStream splitInputStream = null; try { splitInputStream = createSplitInputStream(zipModel); splitInputStream.prepareExtractionForFileHeader(fileHeader); @@ -51,15 +51,14 @@ public static void applyFileAttributes(FileHeader fileHeader, File file) { } } - public static SplitInputStream createSplitInputStream(ZipModel zipModel) throws IOException { + public static SplitFileInputStream createSplitInputStream(ZipModel zipModel) throws IOException { File zipFile = zipModel.getZipFile(); if (zipFile.getName().endsWith(InternalZipConstants.SEVEN_ZIP_SPLIT_FILE_EXTENSION_PATTERN)) { - return new NumberedSplitInputStream(zipModel.getZipFile(), true, - zipModel.getEndOfCentralDirectoryRecord().getNumberOfThisDisk()); + return new NumberedSplitFileInputStream(zipModel.getZipFile()); } - return new ZipStandardSplitInputStream(zipModel.getZipFile(), zipModel.isSplitArchive(), + return new ZipStandardSplitFileInputStream(zipModel.getZipFile(), zipModel.isSplitArchive(), zipModel.getEndOfCentralDirectoryRecord().getNumberOfThisDisk()); } diff --git a/src/main/java/net/lingala/zip4j/util/Zip4jUtil.java b/src/main/java/net/lingala/zip4j/util/Zip4jUtil.java index 39a8f56..91228f0 100644 --- a/src/main/java/net/lingala/zip4j/util/Zip4jUtil.java +++ b/src/main/java/net/lingala/zip4j/util/Zip4jUtil.java @@ -32,6 +32,10 @@ public class Zip4jUtil { private static final long DOSTIME_BEFORE_1980 = (1 << 21) | (1 << 16); private static final int MAX_RAW_READ_FULLY_RETRY_ATTEMPTS = 15; + public static boolean isStringNullOrEmpty(String str) { + return str == null || str.trim().length() == 0; + } + public static boolean isStringNotNullAndNotEmpty(String str) { return str != null && str.trim().length() > 0; } @@ -97,28 +101,19 @@ private static long dosToEpochTime(long dosTime) { return cal.getTime().getTime(); } - public static byte[] convertCharArrayToByteArray(char[] charArray) { - try { - ByteBuffer buf = InternalZipConstants.CHARSET_UTF_8.encode(CharBuffer.wrap(charArray)); - byte[] bytes = new byte[buf.limit()]; - buf.get(bytes); - return bytes; - } catch (Exception e) { - byte[] bytes = new byte[charArray.length]; - for (int i = 0; i < charArray.length; i++) { - bytes[i] = (byte) charArray[i]; - } - return bytes; - } + public static byte[] convertCharArrayToByteArray(char[] charArray, boolean useUtf8Charset) { + return useUtf8Charset + ? convertCharArrayToByteArrayUsingUtf8(charArray) + : convertCharArrayToByteArrayUsingDefaultCharset(charArray); } - public static CompressionMethod getCompressionMethod(AbstractFileHeader localFileHeader) { + public static CompressionMethod getCompressionMethod(AbstractFileHeader localFileHeader) throws ZipException { if (localFileHeader.getCompressionMethod() != CompressionMethod.AES_INTERNAL_ONLY) { return localFileHeader.getCompressionMethod(); } if (localFileHeader.getAesExtraDataRecord() == null) { - throw new RuntimeException("AesExtraDataRecord not present in local header for aes encrypted data"); + throw new ZipException("AesExtraDataRecord not present in local header for aes encrypted data"); } return localFileHeader.getAesExtraDataRecord().getCompressionMethod(); @@ -128,6 +123,10 @@ public static int readFully(InputStream inputStream, byte[] bufferToReadInto) th int readLen = inputStream.read(bufferToReadInto); + if (readLen == -1) { + throw new IOException("Unexpected EOF reached when trying to read stream"); + } + if (readLen != bufferToReadInto.length) { readLen = readUntilBufferIsFull(inputStream, bufferToReadInto, readLen); @@ -175,6 +174,13 @@ public static int readFully(InputStream inputStream, byte[] b, int offset, int l private static int readUntilBufferIsFull(InputStream inputStream, byte[] bufferToReadInto, int readLength) throws IOException { + if (readLength < 0) { + throw new IOException("Invalid readLength"); + } + + if (readLength == 0) { + return 0; + } int remainingLength = bufferToReadInto.length - readLength; int loopReadLength = 0; @@ -197,4 +203,23 @@ private static int readUntilBufferIsFull(InputStream inputStream, byte[] bufferT return readLength; } + private static byte[] convertCharArrayToByteArrayUsingUtf8(char[] charArray) { + try { + ByteBuffer buf = InternalZipConstants.CHARSET_UTF_8.encode(CharBuffer.wrap(charArray)); + byte[] bytes = new byte[buf.limit()]; + buf.get(bytes); + return bytes; + } catch (Exception e) { + return convertCharArrayToByteArrayUsingDefaultCharset(charArray); + } + } + + private static byte[] convertCharArrayToByteArrayUsingDefaultCharset(char[] charArray) { + byte[] bytes = new byte[charArray.length]; + for (int i = 0; i < charArray.length; i++) { + bytes[i] = (byte) charArray[i]; + } + return bytes; + } + } diff --git a/src/main/resources/assets/mcinstanceloader/lang/en_US.lang b/src/main/resources/assets/mcinstanceloader/lang/en_US.lang index b998fa6..4051b52 100644 --- a/src/main/resources/assets/mcinstanceloader/lang/en_US.lang +++ b/src/main/resources/assets/mcinstanceloader/lang/en_US.lang @@ -10,4 +10,5 @@ gui.mcinstanceloader.installupdate=Install update gui.mcinstanceloader.continue=Continue anyways # Optional resources GUI -gui.mcinstanceloader.confirm=Confirm and continue \ No newline at end of file +gui.mcinstanceloader.confirm=Confirm and continue +gui.mcinstanceloader.waitbutton=Downloading, please wait... \ No newline at end of file diff --git a/versionData.txt b/versionData.txt index 9007a38..07a014f 100644 --- a/versionData.txt +++ b/versionData.txt @@ -1,3 +1,3 @@ -version: 2.1 -fileName: mcinstanceloader-2.1.jar +version: 2.2 +fileName: mcinstanceloader-2.2.jar url: https://github.com/HRudyPlayZ/MCInstanceLoader/releases/download/1.7.10-2.1/mcinstanceloader-2.1.jar