From a7ca0f548a333aa6c8c58af957fcb58b9770ddd2 Mon Sep 17 00:00:00 2001 From: "Vladimir.Orlov" Date: Wed, 14 Jan 2015 11:29:07 +0300 Subject: [PATCH] synchronized Updater with the new version from google. Changes: - Strict patches - Directories as files - Support for moving files - Making use of critical files - Zip file normalization for binary patches - Mac patches from the application folder --- .../intellij/updater/BaseDeleteAction.java | 59 --- .../intellij/updater/BaseUpdateAction.java | 79 +++- .../intellij/updater/ConsoleUpdaterUI.java | 11 + .../com/intellij/updater/CreateAction.java | 52 ++- .../com/intellij/updater/DeleteAction.java | 56 ++- .../com/intellij/updater/DeleteZipAction.java | 20 - .../com/intellij/updater/DiffCalculator.java | 132 +++++- .../src/com/intellij/updater/Digester.java | 94 ++-- updater/src/com/intellij/updater/Patch.java | 320 +++++++++---- .../src/com/intellij/updater/PatchAction.java | 55 ++- .../intellij/updater/PatchFileCreator.java | 22 +- .../src/com/intellij/updater/PatchSpec.java | 173 +++++++ .../com/intellij/updater/RetryException.java | 39 ++ updater/src/com/intellij/updater/Runner.java | 192 ++++---- .../com/intellij/updater/SwingUpdaterUI.java | 61 ++- .../com/intellij/updater/UpdateAction.java | 62 +-- .../com/intellij/updater/UpdateZipAction.java | 25 +- .../src/com/intellij/updater/UpdaterUI.java | 10 + updater/src/com/intellij/updater/Utils.java | 115 ++++- .../com/intellij/updater/ValidateAction.java | 56 +++ .../intellij/updater/ValidationResult.java | 4 +- updater/testData/Readme.txt | 10 +- .../com/intellij/updater/DigesterTest.java | 29 +- .../updater/PatchFileCreatorBinaryTest.java | 24 + .../PatchFileCreatorNotBinaryTest.java | 87 ++++ .../updater/PatchFileCreatorTest.java | 442 +++++++++++++----- .../com/intellij/updater/PatchTest.java | 45 +- .../com/intellij/updater/RunnerTest.java | 6 +- .../com/intellij/updater/UpdaterTestCase.java | 28 ++ 29 files changed, 1696 insertions(+), 612 deletions(-) delete mode 100644 updater/src/com/intellij/updater/BaseDeleteAction.java delete mode 100644 updater/src/com/intellij/updater/DeleteZipAction.java create mode 100644 updater/src/com/intellij/updater/PatchSpec.java create mode 100644 updater/src/com/intellij/updater/RetryException.java create mode 100644 updater/src/com/intellij/updater/ValidateAction.java create mode 100644 updater/testSrc/com/intellij/updater/PatchFileCreatorBinaryTest.java create mode 100644 updater/testSrc/com/intellij/updater/PatchFileCreatorNotBinaryTest.java diff --git a/updater/src/com/intellij/updater/BaseDeleteAction.java b/updater/src/com/intellij/updater/BaseDeleteAction.java deleted file mode 100644 index 510ec97c892b..000000000000 --- a/updater/src/com/intellij/updater/BaseDeleteAction.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.intellij.updater; - -import java.io.DataInputStream; -import java.io.File; -import java.io.IOException; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -public abstract class BaseDeleteAction extends PatchAction { - public BaseDeleteAction(String path, long checksum) { - super(path, checksum); - } - - public BaseDeleteAction(DataInputStream in) throws IOException { - super(in); - } - - @Override - public void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { - // do nothing - } - - @Override - protected ValidationResult doValidate(File toFile) throws IOException { - ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.DELETE); - if (result != null) return result; - - if (toFile.exists() && isModified(toFile)) { - return new ValidationResult(ValidationResult.Kind.CONFLICT, - myPath, - ValidationResult.Action.DELETE, - "Modified", - ValidationResult.Option.DELETE, - ValidationResult.Option.KEEP); - } - return null; - } - - @Override - protected boolean shouldApplyOn(File toFile) { - return toFile.exists(); - } - - @Override - protected void doApply(ZipFile patchFile, File toFile) throws IOException { - Utils.delete(toFile); - } - - protected void doBackup(File toFile, File backupFile) throws IOException { - Utils.copy(toFile, backupFile); - } - - protected void doRevert(File toFile, File backupFile) throws IOException { - if (!toFile.exists() || toFile.isDirectory() || isModified(toFile)) { - Utils.delete(toFile); // make sure there is no directory remained on this path (may remain from previous 'create' actions - Utils.copy(backupFile, toFile); - } - } -} diff --git a/updater/src/com/intellij/updater/BaseUpdateAction.java b/updater/src/com/intellij/updater/BaseUpdateAction.java index 1d5e5a1cc78f..49bc87f10c20 100644 --- a/updater/src/com/intellij/updater/BaseUpdateAction.java +++ b/updater/src/com/intellij/updater/BaseUpdateAction.java @@ -7,47 +7,79 @@ import java.io.*; import java.util.zip.ZipOutputStream; public abstract class BaseUpdateAction extends PatchAction { - public BaseUpdateAction(String path, long checksum) { - super(path, checksum); + private final String mySource; + protected final boolean myIsMove; + + public BaseUpdateAction(Patch patch, String path, String source, long checksum, boolean move) { + super(patch, path, checksum); + myIsMove = move; + mySource = source; } - public BaseUpdateAction(DataInputStream in) throws IOException { - super(in); + public BaseUpdateAction(Patch patch, DataInputStream in) throws IOException { + super(patch, in); + mySource = in.readUTF(); + myIsMove = in.readBoolean(); } @Override - protected ValidationResult doValidate(File toFile) throws IOException { - ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.UPDATE); - if (result != null) return result; - return doValidateNotChanged(toFile, ValidationResult.Kind.ERROR, ValidationResult.Action.UPDATE); + public void write(DataOutputStream out) throws IOException { + super.write(out); + out.writeUTF(mySource); + out.writeBoolean(myIsMove); + } + + protected File getSource(File toDir) { + return new File(toDir, mySource); + } + + public String getSourcePath() { + return mySource; } @Override - protected boolean shouldApplyOn(File toFile) { + protected boolean doShouldApply(File toDir) { // if the file is optional in may not exist - return toFile.exists(); + return getSource(toDir).exists(); + } + + @Override + public void buildPatchFile(File olderDir, File newerDir, ZipOutputStream patchOutput) throws IOException { + doBuildPatchFile(getSource(olderDir), getFile(newerDir), patchOutput); + } + + @Override + public ValidationResult validate(File toDir) throws IOException { + File fromFile = getSource(toDir); + ValidationResult result = doValidateAccess(fromFile, ValidationResult.Action.UPDATE); + if (result != null) return result; + if (!mySource.isEmpty()) { + result = doValidateAccess(getFile(toDir), ValidationResult.Action.UPDATE); + if (result != null) return result; + } + return doValidateNotChanged(fromFile, ValidationResult.Kind.ERROR, ValidationResult.Action.UPDATE); } @Override protected void doBackup(File toFile, File backupFile) throws IOException { - Utils.copy(toFile, backupFile); + Utils.mirror(toFile, backupFile); } protected void replaceUpdated(File from, File dest) throws IOException { // on OS X code signing caches seem to be associated with specific file ids, so we need to remove the original file. - if (!dest.delete()) throw new IOException("Cannot delete file " + dest); + if (dest.exists() && !dest.delete()) throw new IOException("Cannot delete file " + dest); Utils.copy(from, dest); } @Override protected void doRevert(File toFile, File backupFile) throws IOException { if (!toFile.exists() || isModified(toFile)) { - Utils.copy(backupFile, toFile); + Utils.mirror(backupFile, toFile); } } - protected void writeDiff(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { - BufferedInputStream olderFileIn = new BufferedInputStream(new FileInputStream(olderFile)); + protected void writeDiff(File olderFile, File newerFile, OutputStream patchOutput) throws IOException { + BufferedInputStream olderFileIn = new BufferedInputStream(Utils.newFileInputStream(olderFile, myPatch.isNormalized())); BufferedInputStream newerFileIn = new BufferedInputStream(new FileInputStream(newerFile)); try { writeDiff(olderFileIn, newerFileIn, patchOutput); @@ -58,14 +90,14 @@ public abstract class BaseUpdateAction extends PatchAction { } } - protected void writeDiff(InputStream olderFileIn, InputStream newerFileIn, ZipOutputStream patchOutput) + protected void writeDiff(InputStream olderFileIn, InputStream newerFileIn, OutputStream patchOutput) throws IOException { Runner.logger.info("writing diff"); ByteArrayOutputStream diffOutput = new ByteArrayOutputStream(); byte[] newerFileBuffer = JBDiff.bsdiff(olderFileIn, newerFileIn, diffOutput); diffOutput.close(); - if (diffOutput.size() < newerFileBuffer.length) { + if (!isCritical() && diffOutput.size() < newerFileBuffer.length) { patchOutput.write(1); Utils.copyBytesToStream(diffOutput, patchOutput); } @@ -83,4 +115,17 @@ public abstract class BaseUpdateAction extends PatchAction { Utils.copyStream(patchInput, toFileOut); } } + + @Override + public String toString() { + String moveInfo = ""; + if (!mySource.equals(myPath)) { + moveInfo = "[" + (myIsMove ? "= " : "~ ") + mySource + "]"; + } + return super.toString() + moveInfo; + } + + public boolean isMove() { + return myIsMove; + } } diff --git a/updater/src/com/intellij/updater/ConsoleUpdaterUI.java b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java index c1114dd173f6..e96f90d15d5d 100644 --- a/updater/src/com/intellij/updater/ConsoleUpdaterUI.java +++ b/updater/src/com/intellij/updater/ConsoleUpdaterUI.java @@ -31,6 +31,17 @@ public class ConsoleUpdaterUI implements UpdaterUI { public void checkCancelled() throws OperationCancelledException { } + @Override + public void setDescription(String oldBuildDesc, String newBuildDesc) { + System.out.println("From " + oldBuildDesc + " to " + newBuildDesc); + } + + @Override + public boolean showWarning(String message) { + System.out.println("Warning: " + message); + return false; + } + public Map askUser(List validationResults) { return Collections.emptyMap(); } diff --git a/updater/src/com/intellij/updater/CreateAction.java b/updater/src/com/intellij/updater/CreateAction.java index dd44c35f5df1..67794c0b5409 100644 --- a/updater/src/com/intellij/updater/CreateAction.java +++ b/updater/src/com/intellij/updater/CreateAction.java @@ -9,35 +9,40 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class CreateAction extends PatchAction { - public CreateAction(String path) { - super(path, -1); + public CreateAction(Patch patch, String path) { + super(patch, path, Digester.INVALID); } - public CreateAction(DataInputStream in) throws IOException { - super(in); + public CreateAction(Patch patch, DataInputStream in) throws IOException { + super(patch, in); } + @Override protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { Runner.logger.info("building PatchFile"); patchOutput.putNextEntry(new ZipEntry(myPath)); - - writeExecutableFlag(patchOutput, newerFile); - Utils.copyFileToStream(newerFile, patchOutput); + if (!newerFile.isDirectory()) { + writeExecutableFlag(patchOutput, newerFile); + Utils.copyFileToStream(newerFile, patchOutput); + } patchOutput.closeEntry(); } @Override - protected ValidationResult doValidate(File toFile) { + public ValidationResult validate(File toDir) { + File toFile = getFile(toDir); ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.CREATE); if (result != null) return result; if (toFile.exists()) { - return new ValidationResult(ValidationResult.Kind.CONFLICT, - myPath, + ValidationResult.Option[] options = myPatch.isStrict() + ? new ValidationResult.Option[]{ValidationResult.Option.REPLACE} + : new ValidationResult.Option[]{ValidationResult.Option.REPLACE, ValidationResult.Option.KEEP}; + return new ValidationResult(ValidationResult.Kind.CONFLICT, myPath, ValidationResult.Action.CREATE, ValidationResult.ALREADY_EXISTS_MESSAGE, - ValidationResult.Option.REPLACE, ValidationResult.Option.KEEP); + options); } return null; } @@ -48,17 +53,24 @@ public class CreateAction extends PatchAction { } @Override - protected void doApply(ZipFile patchFile, File toFile) throws IOException { + protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException { prepareToWriteFile(toFile); - InputStream in = Utils.getEntryInputStream(patchFile, myPath); - try { - boolean executable = readExecutableFlag(in); - Utils.copyStreamToFile(in, toFile); - Utils.setExecutable(toFile, executable); - } - finally { - in.close(); + ZipEntry entry = Utils.getZipEntry(patchFile, myPath); + if (entry.isDirectory()) { + if (!toFile.mkdir()) { + throw new IOException("Unable to create directory " + myPath); + } + } else { + InputStream in = Utils.findEntryInputStreamForEntry(patchFile, entry); + try { + boolean executable = readExecutableFlag(in); + Utils.copyStreamToFile(in, toFile); + Utils.setExecutable(toFile, executable); + } + finally { + in.close(); + } } } diff --git a/updater/src/com/intellij/updater/DeleteAction.java b/updater/src/com/intellij/updater/DeleteAction.java index e197cbded8e2..6809cf134ae8 100644 --- a/updater/src/com/intellij/updater/DeleteAction.java +++ b/updater/src/com/intellij/updater/DeleteAction.java @@ -3,18 +3,60 @@ package com.intellij.updater; import java.io.DataInputStream; import java.io.File; import java.io.IOException; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; -public class DeleteAction extends BaseDeleteAction { - public DeleteAction(String path, long checksum) { - super(path, checksum); +public class DeleteAction extends PatchAction { + public DeleteAction(Patch patch, String path, long checksum) { + super(patch, path, checksum); } - public DeleteAction(DataInputStream in) throws IOException { - super(in); + public DeleteAction(Patch patch, DataInputStream in) throws IOException { + super(patch, in); } @Override - protected boolean isModified(File toFile) throws IOException { - return myChecksum != Digester.digestRegularFile(toFile); + public void doBuildPatchFile(File olderDir, File newerFile, ZipOutputStream patchOutput) throws IOException { + // do nothing + } + + @Override + public ValidationResult validate(File toDir) throws IOException { + File toFile = getFile(toDir); + ValidationResult result = doValidateAccess(toFile, ValidationResult.Action.DELETE); + if (result != null) return result; + + if (myPatch.validateDeletion(myPath) && toFile.exists() && isModified(toFile)) { + ValidationResult.Option[] options = myPatch.isStrict() + ? new ValidationResult.Option[]{ValidationResult.Option.DELETE} + : new ValidationResult.Option[]{ValidationResult.Option.DELETE, ValidationResult.Option.KEEP}; + ValidationResult.Action action = myChecksum == Digester.INVALID ? ValidationResult.Action.VALIDATE : ValidationResult.Action.DELETE; + String message = myChecksum == Digester.INVALID ? "Unexpected file" : "Modified"; + return new ValidationResult(ValidationResult.Kind.CONFLICT, myPath, action, message, options); + } + return null; + } + + @Override + protected boolean doShouldApply(File toDir) { + return getFile(toDir).exists(); + } + + @Override + protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException { + Utils.delete(toFile); + } + + @Override + protected void doBackup(File toFile, File backupFile) throws IOException { + Utils.copy(toFile, backupFile); + } + + @Override + protected void doRevert(File toFile, File backupFile) throws IOException { + if (!toFile.exists() || toFile.isDirectory() || isModified(toFile)) { + Utils.delete(toFile); // make sure there is no directory remained on this path (may remain from previous 'create' actions + Utils.copy(backupFile, toFile); + } } } diff --git a/updater/src/com/intellij/updater/DeleteZipAction.java b/updater/src/com/intellij/updater/DeleteZipAction.java deleted file mode 100644 index 0cd46d9fa9e5..000000000000 --- a/updater/src/com/intellij/updater/DeleteZipAction.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.intellij.updater; - -import java.io.DataInputStream; -import java.io.File; -import java.io.IOException; - -public class DeleteZipAction extends BaseDeleteAction { - public DeleteZipAction(String path, long checksum) { - super(path, checksum); - } - - public DeleteZipAction(DataInputStream in) throws IOException { - super(in); - } - - @Override - protected boolean isModified(File toFile) throws IOException { - return myChecksum != Digester.digestFile(toFile); - } -} diff --git a/updater/src/com/intellij/updater/DiffCalculator.java b/updater/src/com/intellij/updater/DiffCalculator.java index 4db0033740e9..f5e04b5630ee 100644 --- a/updater/src/com/intellij/updater/DiffCalculator.java +++ b/updater/src/com/intellij/updater/DiffCalculator.java @@ -1,42 +1,148 @@ package com.intellij.updater; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; +import java.io.File; +import java.util.*; public class DiffCalculator { - public static Result calculate(Map oldChecksums, Map newChecksums) { + public static Result calculate(Map oldChecksums, Map newChecksums, List critical, boolean move) { Result result = new Result(); + result.commonFiles = collect(oldChecksums, newChecksums, critical, true); result.filesToDelete = withAllRemoved(oldChecksums, newChecksums); - result.filesToCreate = withAllRemoved(newChecksums, oldChecksums).keySet(); - result.filesToUpdate = collect(oldChecksums, newChecksums, false); + + Map toUpdate = collect(oldChecksums, newChecksums, critical, false); + Map toCreate = withAllRemoved(newChecksums, oldChecksums); + + // Some creates become updates if found in different directories. + result.filesToCreate = new LinkedHashMap(); + result.filesToUpdate = new LinkedHashMap(); + + for (Map.Entry update : toUpdate.entrySet()) { + result.filesToUpdate.put(update.getKey(), new Update(update.getKey(), update.getValue(), false)); + } + + if (move) { + Map byContent = inverse(result.filesToDelete); + Map> byName = groupFilesByName(result.filesToDelete); + + // Find first by content + for (Map.Entry create : toCreate.entrySet()) { + boolean isDir = create.getKey().endsWith("/"); + String source = byContent.get(create.getValue()); + boolean found = false; + if (source != null && !isDir) { + // Found a file with the same content use it, unless it's critical + if (!critical.contains(source)) { + result.filesToUpdate.put(create.getKey(), new Update(source, result.filesToDelete.get(source), true)); + found = true; + } + } + else { + File fileToCreate = new File(create.getKey()); + List sameName = byName.get(fileToCreate.getName()); + if (sameName != null && !isDir) { + String best = findBestCandidateForMove(sameName, create.getKey()); + // Found a file with the same name, if it's not critical use it, worst case as big as a create. + if (!critical.contains(best)) { + result.filesToUpdate.put(create.getKey(), new Update(best, result.filesToDelete.get(best), false)); + found = true; + } + } + } + if (!found) { + // Fine, just create it. + result.filesToCreate.put(create.getKey(), create.getValue()); + } + } + } else { + result.filesToCreate = toCreate; + } + return result; } + private static String findBestCandidateForMove(List paths, String path) { + int common = 0; + String best = ""; + String[] dirs = path.split("/"); + for (String other : paths) { + String[] others = other.split("/"); + for (int i = 0; i < dirs.length && i < others.length; i++) { + if (dirs[dirs.length - i - 1].equals(others[others.length - i - 1])) { + if (i + 1 > common) { + best = other; + common = i + 1; + } + } else { + break; + } + } + } + return best; + } + + private static Map> groupFilesByName(Map toDelete) { + Map> result = new HashMap>(); + for (String path : toDelete.keySet()) { + if (!path.endsWith("/")) { + String name = new File(path).getName(); + List paths = result.get(name); + if (paths == null) { + paths = new LinkedList(); + result.put(name, paths); + } + paths.add(path); + } + } + return result; + } + + public static Map inverse(Map map) { + Map inv = new LinkedHashMap(); + for (Map.Entry entry : map.entrySet()) { + inv.put(entry.getValue(), entry.getKey()); + } + return inv; + } + private static Map withAllRemoved(Map from, Map toRemove) { - Map result = new HashMap(from); + Map result = new LinkedHashMap(from); for (String each : toRemove.keySet()) { result.remove(each); } return result; } - private static Map collect(Map older, Map newer, boolean equal) { - Map result = new HashMap(); + private static Map collect(Map older, Map newer, List critical, boolean equal) { + Map result = new LinkedHashMap(); for (Map.Entry each : newer.entrySet()) { String file = each.getKey(); Long oldChecksum = older.get(file); Long newChecksum = newer.get(file); - if (oldChecksum != null && newChecksum != null && oldChecksum.equals(newChecksum) == equal) { - result.put(file, oldChecksum); + if (oldChecksum != null && newChecksum != null) { + if ((oldChecksum.equals(newChecksum) && !critical.contains(file)) == equal) { + result.put(file, oldChecksum); + } } } return result; } + public static class Update { + public final String source; + public final long checksum; + public final boolean move; + + public Update(String source, long checksum, boolean move) { + this.checksum = checksum; + this.source = source; + this.move = move; + } + } + public static class Result { public Map filesToDelete; - public Set filesToCreate; - public Map filesToUpdate; + public Map filesToCreate; + public Map filesToUpdate; + public Map commonFiles; } } diff --git a/updater/src/com/intellij/updater/Digester.java b/updater/src/com/intellij/updater/Digester.java index b84c33e3f6d5..ba45c49f349e 100644 --- a/updater/src/com/intellij/updater/Digester.java +++ b/updater/src/com/intellij/updater/Digester.java @@ -4,46 +4,19 @@ import java.io.*; import java.util.*; import java.util.zip.CRC32; import java.util.zip.ZipEntry; +import java.util.zip.ZipException; import java.util.zip.ZipFile; public class Digester { - public static Map digestFiles(File dir, List ignoredFiles, UpdaterUI ui) - throws IOException, OperationCancelledException { - Map result = new HashMap(); + // CRC32 will only use the lower 32bits of long, never returning negative values. + public static long INVALID = -1; + public static long DIRECTORY = -2; - LinkedHashSet paths = Utils.collectRelativePaths(dir); - for (String each : paths) { - if (ignoredFiles.contains(each)) continue; - ui.setStatus(each); - ui.checkCancelled(); - result.put(each, digestFile(new File(dir, each))); + public static long digestRegularFile(File file, boolean normalize) throws IOException { + if (file.isDirectory()) { + return DIRECTORY; } - return result; - } - - public static long digestFile(File file) throws IOException { - if (!Runner.ZIP_AS_BINARY && Utils.isZipFile(file.getName())) { - ZipFile zipFile; - try { - zipFile = new ZipFile(file); - } - catch (IOException e) { - Runner.printStackTrace(e); - return digestRegularFile(file); - } - - try { - return doDigestZipFile(zipFile); - } - finally { - zipFile.close(); - } - } - return digestRegularFile(file); - } - - public static long digestRegularFile(File file) throws IOException { - InputStream in = new BufferedInputStream(new FileInputStream(file)); + InputStream in = new BufferedInputStream(Utils.newFileInputStream(file, normalize)); try { return digestStream(in); } @@ -52,32 +25,43 @@ public class Digester { } } - private static long doDigestZipFile(ZipFile zipFile) throws IOException { - List sorted = new ArrayList(); - - Enumeration temp = zipFile.entries(); - while (temp.hasMoreElements()) { - ZipEntry each = temp.nextElement(); - if (!each.isDirectory()) sorted.add(each); + public static long digestZipFile(File file) throws IOException { + ZipFile zipFile; + try { + zipFile = new ZipFile(file); + } catch (ZipException e) { + // This was not a zip file... + return digestRegularFile(file, false); } + try { + List sorted = new ArrayList(); - Collections.sort(sorted, new Comparator() { - public int compare(ZipEntry o1, ZipEntry o2) { - return o1.getName().compareTo(o2.getName()); + Enumeration temp = zipFile.entries(); + while (temp.hasMoreElements()) { + ZipEntry each = temp.nextElement(); + if (!each.isDirectory()) sorted.add(each); } - }); - CRC32 crc = new CRC32(); - for (ZipEntry each : sorted) { - InputStream in = zipFile.getInputStream(each); - try { - doDigestStream(in, crc); - } - finally { - in.close(); + Collections.sort(sorted, new Comparator() { + public int compare(ZipEntry o1, ZipEntry o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + + CRC32 crc = new CRC32(); + for (ZipEntry each : sorted) { + InputStream in = zipFile.getInputStream(each); + try { + doDigestStream(in, crc); + } + finally { + in.close(); + } } + return crc.getValue(); + } finally { + zipFile.close(); } - return crc.getValue(); } public static long digestStream(InputStream in) throws IOException { diff --git a/updater/src/com/intellij/updater/Patch.java b/updater/src/com/intellij/updater/Patch.java index d9a33a6df24d..be237a443afd 100644 --- a/updater/src/com/intellij/updater/Patch.java +++ b/updater/src/com/intellij/updater/Patch.java @@ -6,64 +6,75 @@ import java.util.zip.ZipFile; public class Patch { private List myActions = new ArrayList(); + private boolean myIsBinary; + private boolean myIsStrict; + private boolean myIsNormalized; + private String myOldBuild; + private String myNewBuild; + private String myRoot; + private Map myWarnings; + private List myDeleteFiles; private static final int CREATE_ACTION_KEY = 1; private static final int UPDATE_ACTION_KEY = 2; private static final int UPDATE_ZIP_ACTION_KEY = 3; private static final int DELETE_ACTION_KEY = 4; - private static final int DELETE_ZIP_ACTION_KEY = 5; + private static final int VALIDATE_ACTION_KEY = 5; - public Patch(File olderDir, - File newerDir, - List ignoredFiles, - List criticalFiles, - List optionalFiles, - UpdaterUI ui) throws IOException, OperationCancelledException { - calculateActions(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui); + public Patch(PatchSpec spec, UpdaterUI ui) throws IOException, OperationCancelledException { + myIsBinary = spec.isBinary(); + myIsStrict = spec.isStrict(); + myIsNormalized = spec.isNormalized(); + myOldBuild = spec.getOldVersionDescription(); + myNewBuild = spec.getNewVersionDescription(); + myWarnings = spec.getWarnings(); + myDeleteFiles = spec.getDeleteFiles(); + myRoot = spec.getRoot(); + + calculateActions(spec, ui); } public Patch(InputStream patchIn) throws IOException { read(patchIn); } - private void calculateActions(File olderDir, - File newerDir, - List ignoredFiles, - List criticalFiles, - List optionalFiles, - UpdaterUI ui) - throws IOException, OperationCancelledException { - DiffCalculator.Result diff; - + private void calculateActions(PatchSpec spec, UpdaterUI ui) throws IOException, OperationCancelledException { Runner.logger.info("Calculating difference..."); ui.startProcess("Calculating difference..."); ui.checkCancelled(); - diff = DiffCalculator.calculate(Digester.digestFiles(olderDir, ignoredFiles, ui), - Digester.digestFiles(newerDir, ignoredFiles, ui)); + File olderDir = new File(spec.getOldFolder()); + File newerDir = new File(spec.getNewFolder()); + DiffCalculator.Result diff; + diff = DiffCalculator.calculate(digestFiles(olderDir, spec.getIgnoredFiles(), isNormalized(), ui), + digestFiles(newerDir, spec.getIgnoredFiles(), false, ui), + spec.getCriticalFiles(), true); List tempActions = new ArrayList(); // 'delete' actions before 'create' actions to prevent newly created files to be deleted if the names differ only on case. for (Map.Entry each : diff.filesToDelete.entrySet()) { - if (!Runner.ZIP_AS_BINARY && Utils.isZipFile(each.getKey())) { - tempActions.add(new DeleteZipAction(each.getKey(), each.getValue())); - } else - { - tempActions.add(new DeleteAction(each.getKey(), each.getValue())); - } + // Add them in reverse order so directory structures start deleting the files before the directory itself. + tempActions.add(0, new DeleteAction(this, each.getKey(), each.getValue())); } - for (String each : diff.filesToCreate) { - tempActions.add(new CreateAction(each)); + for (String each : diff.filesToCreate.keySet()) { + tempActions.add(new CreateAction(this, each)); } - for (Map.Entry each : diff.filesToUpdate.entrySet()) { - if (!Runner.ZIP_AS_BINARY && Utils.isZipFile(each.getKey())) { - tempActions.add(new UpdateZipAction(each.getKey(), each.getValue())); + for (Map.Entry each : diff.filesToUpdate.entrySet()) { + DiffCalculator.Update update = each.getValue(); + if (!spec.isBinary() && Utils.isZipFile(each.getKey())) { + tempActions.add(new UpdateZipAction(this, each.getKey(), update.source, update.checksum, update.move)); } else { - tempActions.add(new UpdateAction(each.getKey(), each.getValue())); + tempActions.add(new UpdateAction(this, each.getKey(), update.source, update.checksum, update.move)); + } + } + + if (spec.isStrict()) { + for (Map.Entry each : diff.commonFiles.entrySet()) { + tempActions.add(new ValidateAction(this, each.getKey(), each.getValue())); } } @@ -77,8 +88,8 @@ public class Patch { if (!each.calculate(olderDir, newerDir)) continue; myActions.add(each); - each.setCritical(criticalFiles.contains(each.getPath())); - each.setOptional(optionalFiles.contains(each.getPath())); + each.setCritical(spec.getCriticalFiles().contains(each.getPath())); + each.setOptional(spec.getOptionalFiles().contains(each.getPath())); } } @@ -89,104 +100,189 @@ public class Patch { public void write(OutputStream out) throws IOException { @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataOutputStream dataOut = new DataOutputStream(out); try { - dataOut.writeInt(myActions.size()); - - for (PatchAction each : myActions) { - int key; - Class clazz = each.getClass(); - - if (clazz == CreateAction.class) { - key = CREATE_ACTION_KEY; - } - else if (clazz == UpdateAction.class) { - key = UPDATE_ACTION_KEY; - } - else if (clazz == UpdateZipAction.class) { - key = UPDATE_ZIP_ACTION_KEY; - } - else if (clazz == DeleteZipAction.class) { - key = DELETE_ZIP_ACTION_KEY; - } - else if (clazz == DeleteAction.class) { - key = DELETE_ACTION_KEY; - } - else { - throw new RuntimeException("Unknown action " + each); - } - dataOut.writeInt(key); - each.write(dataOut); - } + dataOut.writeUTF(myOldBuild); + dataOut.writeUTF(myNewBuild); + dataOut.writeUTF(myRoot); + dataOut.writeBoolean(myIsBinary); + dataOut.writeBoolean(myIsStrict); + dataOut.writeBoolean(myIsNormalized); + writeMap(dataOut, myWarnings); + writeList(dataOut, myDeleteFiles); + writeActions(dataOut, myActions); } finally { dataOut.flush(); } } + private static void writeList(DataOutputStream dataOut, List list) throws IOException { + dataOut.writeInt(list.size()); + for (String string : list) { + dataOut.writeUTF(string); + } + } + + private static void writeMap(DataOutputStream dataOut, Map map) throws IOException { + dataOut.writeInt(map.size()); + for (Map.Entry entry : map.entrySet()) { + dataOut.writeUTF(entry.getKey()); + dataOut.writeUTF(entry.getValue()); + } + } + + private void writeActions(DataOutputStream dataOut, List actions) throws IOException { + dataOut.writeInt(actions.size()); + + for (PatchAction each : actions) { + int key; + Class clazz = each.getClass(); + + if (clazz == CreateAction.class) { + key = CREATE_ACTION_KEY; + } + else if (clazz == UpdateAction.class) { + key = UPDATE_ACTION_KEY; + } + else if (clazz == UpdateZipAction.class) { + key = UPDATE_ZIP_ACTION_KEY; + } + else if (clazz == DeleteAction.class) { + key = DELETE_ACTION_KEY; + } + else if (clazz == ValidateAction.class) { + key = VALIDATE_ACTION_KEY; + } + else { + throw new RuntimeException("Unknown action " + each); + } + dataOut.writeInt(key); + each.write(dataOut); + } + } + private void read(InputStream patchIn) throws IOException { - List newActions = new ArrayList(); @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") DataInputStream in = new DataInputStream(patchIn); - int size = in.readInt(); + myOldBuild = in.readUTF(); + myNewBuild = in.readUTF(); + myRoot = in.readUTF(); + myIsBinary = in.readBoolean(); + myIsStrict = in.readBoolean(); + myIsNormalized = in.readBoolean(); + myWarnings = readMap(in); + myDeleteFiles = readList(in); + myActions = readActions(in); + } + + private static List readList(DataInputStream in) throws IOException { + int size = in.readInt(); + List list = new ArrayList(size); + for (int i = 0; i < size; i++) { + list.add(in.readUTF()); + } + return list; + } + + private static Map readMap(DataInputStream in) throws IOException { + int size = in.readInt(); + Map map = new HashMap(); + for (int i = 0; i < size; i++) { + String key = in.readUTF(); + map.put(key, in.readUTF()); + } + return map; + } + + private List readActions(DataInputStream in) throws IOException { + List actions = new ArrayList(); + int size = in.readInt(); while (size-- > 0) { int key = in.readInt(); PatchAction a; switch (key) { case CREATE_ACTION_KEY: - a = new CreateAction(in); + a = new CreateAction(this, in); break; case UPDATE_ACTION_KEY: - a = new UpdateAction(in); + a = new UpdateAction(this, in); break; case UPDATE_ZIP_ACTION_KEY: - a = new UpdateZipAction(in); + a = new UpdateZipAction(this, in); break; case DELETE_ACTION_KEY: - a = new DeleteAction(in); + a = new DeleteAction(this, in); break; - case DELETE_ZIP_ACTION_KEY: - a = new DeleteZipAction(in); + case VALIDATE_ACTION_KEY: + a = new ValidateAction(this, in); break; default: throw new RuntimeException("Unknown action type " + key); } - newActions.add(a); + actions.add(a); } - - myActions = newActions; + return actions; } - public List validate(final File toDir, UpdaterUI ui) throws IOException, OperationCancelledException { - final LinkedHashSet files = Utils.collectRelativePaths(toDir); + private File toBaseDir(File toDir) throws IOException { + // This removes myRoot from the end of toDir. myRoot is expressed with '/' so converting to URI to normalize separators. + String path = toDir.toURI().getPath(); + if (!path.endsWith(myRoot)) { + throw new IOException("The patch must be applied to the root folder " + myRoot); + } + return new File(path.substring(0, path.length() - myRoot.length())); + } + + public List validate(final File rootDir, UpdaterUI ui) throws IOException, OperationCancelledException { + LinkedHashSet files = null; + final File toDir = toBaseDir(rootDir); + boolean checkWarnings = true; + while (checkWarnings) { + files = Utils.collectRelativePaths(toDir, myIsStrict); + checkWarnings = false; + for (String file : files) { + String warning = myWarnings.get(file); + if (warning != null) { + if (!ui.showWarning(warning)) { + throw new OperationCancelledException(); + } + checkWarnings = true; + break; + } + } + } + final List result = new ArrayList(); + if (myIsStrict) { + // In strict mode add delete actions for unknown files. + for (PatchAction action : myActions) { + files.remove(action.getPath()); + } + for (String file : files) { + myActions.add(0, new DeleteAction(this, file, Digester.INVALID)); + } + } Runner.logger.info("Validating installation..."); forEach(myActions, "Validating installation...", ui, true, new ActionsProcessor() { + @Override public void forEach(PatchAction each) throws IOException { ValidationResult validationResult = each.validate(toDir); if (validationResult != null) result.add(validationResult); - files.remove(each.getPath()); } }); - //for (String each : files) { - // result.add(new ValidationResult(ValidationResult.Kind.INFO, - // each, - // ValidationResult.Action.NO_ACTION, - // ValidationResult.MANUALLY_ADDED_MESSAGE, - // ValidationResult.Option.KEEP, ValidationResult.Option.DELETE)); - //} - return result; } public ApplicationResult apply(final ZipFile patchFile, - final File toDir, + final File rootDir, final File backupDir, final Map options, UpdaterUI ui) throws IOException, OperationCancelledException { + final File toDir = toBaseDir(rootDir); List actionsToProcess = new ArrayList(); for (PatchAction each : myActions) { if (each.shouldApply(toDir, options)) actionsToProcess.add(each); @@ -194,6 +290,7 @@ public class Patch { forEach(actionsToProcess, "Backing up files...", ui, true, new ActionsProcessor() { + @Override public void forEach(PatchAction each) throws IOException { each.backup(toDir, backupDir); } @@ -205,9 +302,10 @@ public class Patch { try { forEach(actionsToProcess, "Applying patch...", ui, true, new ActionsProcessor() { + @Override public void forEach(PatchAction each) throws IOException { appliedActions.add(each); - each.apply(patchFile, toDir); + each.apply(patchFile, backupDir, toDir); } }); } @@ -223,7 +321,7 @@ public class Patch { } if (shouldRevert) { - revert(appliedActions, backupDir, toDir, ui); + revert(appliedActions, backupDir, rootDir, ui); appliedActions.clear(); if (cancelled) throw new OperationCancelledException(); @@ -235,11 +333,13 @@ public class Patch { return new ApplicationResult(appliedActions); } - public void revert(List actions, final File backupDir, final File toDir, UpdaterUI ui) + public void revert(List actions, final File backupDir, final File rootDir, UpdaterUI ui) throws OperationCancelledException, IOException { Collections.reverse(actions); + final File toDir = toBaseDir(rootDir); forEach(actions, "Reverting...", ui, false, new ActionsProcessor() { + @Override public void forEach(PatchAction each) throws IOException { each.revert(toDir, backupDir); } @@ -263,6 +363,54 @@ public class Patch { } } + public long digestFile(File toFile, boolean normalize) throws IOException { + if (!myIsBinary && Utils.isZipFile(toFile.getName())) { + return Digester.digestZipFile(toFile); + } + else { + return Digester.digestRegularFile(toFile, normalize); + } + } + + public Map digestFiles(File dir, List ignoredFiles, boolean normalize, UpdaterUI ui) + throws IOException, OperationCancelledException { + Map result = new LinkedHashMap(); + + LinkedHashSet paths = Utils.collectRelativePaths(dir, myIsStrict); + for (String each : paths) { + if (ignoredFiles.contains(each)) continue; + ui.setStatus(each); + ui.checkCancelled(); + result.put(each, digestFile(new File(dir, each), normalize)); + } + return result; + } + + public String getOldBuild() { + return myOldBuild; + } + + public String getNewBuild() { + return myNewBuild; + } + + public boolean isStrict() { + return myIsStrict; + } + + public boolean isNormalized() { + return myIsNormalized; + } + + public boolean validateDeletion(String path) { + for (String delete : myDeleteFiles) { + if (path.matches(delete)) { + return false; + } + } + return true; + } + public interface ActionsProcessor { void forEach(PatchAction each) throws IOException; } diff --git a/updater/src/com/intellij/updater/PatchAction.java b/updater/src/com/intellij/updater/PatchAction.java index e91330eae3bd..bf7bf27c91e1 100644 --- a/updater/src/com/intellij/updater/PatchAction.java +++ b/updater/src/com/intellij/updater/PatchAction.java @@ -15,13 +15,16 @@ public abstract class PatchAction { protected long myChecksum; private boolean isCritical; private boolean isOptional; + protected transient Patch myPatch; - public PatchAction(String path, long checksum) { - myPath = path; + public PatchAction(Patch patch, String path, long checksum) { + myPatch = patch; myChecksum = checksum; + myPath = path; } - public PatchAction(DataInputStream in) throws IOException { + public PatchAction(Patch patch, DataInputStream in) throws IOException { + myPatch = patch; myPath = in.readUTF(); myChecksum = in.readLong(); isCritical = in.readBoolean(); @@ -70,18 +73,14 @@ public abstract class PatchAction { process.terminate(); } } - return shouldApplyOn(file); + return doShouldApply(toDir); } - protected boolean shouldApplyOn(File toFile) { + protected boolean doShouldApply(File toDir) { return true; } - public ValidationResult validate(File toDir) throws IOException { - return doValidate(getFile(toDir)); - } - - protected abstract ValidationResult doValidate(final File toFile) throws IOException; + protected abstract ValidationResult validate(File toDir) throws IOException; protected ValidationResult doValidateAccess(File toFile, ValidationResult.Action action) { if (!toFile.exists()) return null; @@ -95,7 +94,7 @@ public abstract class PatchAction { myPath, action, ValidationResult.ACCESS_DENIED_MESSAGE, - ValidationResult.Option.IGNORE); + myPatch.isStrict() ? ValidationResult.Option.NONE : ValidationResult.Option.IGNORE); } private boolean isWritable(File toFile) { @@ -144,11 +143,27 @@ public abstract class PatchAction { throws IOException { if (toFile.exists()) { if (isModified(toFile)) { + ValidationResult.Option[] options; + if (myPatch.isStrict()) { + if (isCritical) { + options = new ValidationResult.Option[]{ ValidationResult.Option.REPLACE }; + } + else { + options = new ValidationResult.Option[]{ ValidationResult.Option.NONE }; + } + } else { + if (isCritical) { + options = new ValidationResult.Option[]{ ValidationResult.Option.REPLACE, ValidationResult.Option.IGNORE }; + } + else { + options = new ValidationResult.Option[]{ ValidationResult.Option.IGNORE }; + } + } return new ValidationResult(kind, myPath, action, ValidationResult.MODIFIED_MESSAGE, - ValidationResult.Option.IGNORE); + options); } } else if (!isOptional) { @@ -156,18 +171,20 @@ public abstract class PatchAction { myPath, action, ValidationResult.ABSENT_MESSAGE, - ValidationResult.Option.IGNORE); + myPatch.isStrict() ? ValidationResult.Option.NONE : ValidationResult.Option.IGNORE); } return null; } - abstract protected boolean isModified(File toFile) throws IOException; - - public void apply(ZipFile patchFile, File toDir) throws IOException { - doApply(patchFile, getFile(toDir)); + protected boolean isModified(File toFile) throws IOException { + return myChecksum == Digester.INVALID || myChecksum != myPatch.digestFile(toFile, myPatch.isNormalized()); } - protected abstract void doApply(ZipFile patchFile, File toFile) throws IOException; + public void apply(ZipFile patchFile, File backupDir, File toDir) throws IOException { + doApply(patchFile, backupDir, getFile(toDir)); + } + + protected abstract void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException; public void backup(File toDir, File backupDir) throws IOException { doBackup(getFile(toDir), getFile(backupDir)); @@ -181,7 +198,7 @@ public abstract class PatchAction { protected abstract void doRevert(File toFile, File backupFile) throws IOException; - private File getFile(File baseDir) { + protected File getFile(File baseDir) { return new File(baseDir, myPath); } diff --git a/updater/src/com/intellij/updater/PatchFileCreator.java b/updater/src/com/intellij/updater/PatchFileCreator.java index e8892375e204..6e1ed5c6b4ae 100644 --- a/updater/src/com/intellij/updater/PatchFileCreator.java +++ b/updater/src/com/intellij/updater/PatchFileCreator.java @@ -1,9 +1,6 @@ package com.intellij.updater; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; @@ -13,15 +10,9 @@ import java.util.zip.ZipOutputStream; public class PatchFileCreator { private static final String PATCH_INFO_FILE_NAME = ".patch-info"; - public static void create(File olderDir, - File newerDir, - File patchFile, - List ignoredFiles, - List criticalFiles, - List optionalFiles, - UpdaterUI ui) throws IOException, OperationCancelledException { + public static Patch create(PatchSpec spec, File patchFile, UpdaterUI ui) throws IOException, OperationCancelledException { - Patch patchInfo = new Patch(olderDir, newerDir, ignoredFiles, criticalFiles, optionalFiles, ui); + Patch patchInfo = new Patch(spec, ui); Runner.logger.info("Creating the patch file '" + patchFile + "'..."); ui.startProcess("Creating the patch file '" + patchFile + "'..."); ui.checkCancelled(); @@ -34,6 +25,8 @@ public class PatchFileCreator { patchInfo.write(out); out.closeEntry(); + File olderDir = new File(spec.getOldFolder()); + File newerDir = new File(spec.getNewFolder()); List actions = patchInfo.getActions(); for (PatchAction each : actions) { @@ -46,6 +39,8 @@ public class PatchFileCreator { finally { out.close(); } + + return patchInfo; } public static PreparationResult prepareAndValidate(File patchFile, @@ -55,6 +50,7 @@ public class PatchFileCreator { ZipFile zipFile = new ZipFile(patchFile); try { + InputStream in = Utils.getEntryInputStream(zipFile, PATCH_INFO_FILE_NAME); try { patch = new Patch(in); @@ -67,6 +63,8 @@ public class PatchFileCreator { zipFile.close(); } + ui.setDescription(patch.getOldBuild(), patch.getNewBuild()); + List validationResults = patch.validate(toDir, ui); return new PreparationResult(patch, patchFile, toDir, validationResults); } diff --git a/updater/src/com/intellij/updater/PatchSpec.java b/updater/src/com/intellij/updater/PatchSpec.java new file mode 100644 index 000000000000..b4daf67e9dad --- /dev/null +++ b/updater/src/com/intellij/updater/PatchSpec.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.updater; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class PatchSpec { + private String myOldVersionDescription = ""; + private String myNewVersionDescription = ""; + private String myOldFolder; + private String myNewFolder; + private String myPatchFile; + private String myJarFile; + private boolean myIsBinary; + private boolean myIsStrict; + private List myIgnoredFiles = Collections.emptyList(); + private List myCriticalFiles = Collections.emptyList(); + private List myOptionalFiles = Collections.emptyList(); + private boolean myIsNormalized; + private Map myWarnings = Collections.emptyMap(); + private List myDeleteFiles = Collections.emptyList(); + private String myRoot = ""; + + public String getOldVersionDescription() { + return myOldVersionDescription; + } + + public PatchSpec setOldVersionDescription(String oldVersionDescription) { + myOldVersionDescription = oldVersionDescription; + return this; + } + + public String getNewVersionDescription() { + return myNewVersionDescription; + } + + public PatchSpec setNewVersionDescription(String newVersionDescription) { + myNewVersionDescription = newVersionDescription; + return this; + } + + public String getOldFolder() { + return myOldFolder; + } + + public PatchSpec setOldFolder(String oldFolder) { + myOldFolder = oldFolder; + return this; + } + + public String getNewFolder() { + return myNewFolder; + } + + public PatchSpec setNewFolder(String newFolder) { + myNewFolder = newFolder; + return this; + } + + public String getPatchFile() { + return myPatchFile; + } + + public PatchSpec setPatchFile(String patchFile) { + myPatchFile = patchFile; + return this; + } + + public String getJarFile() { + return myJarFile; + } + + public PatchSpec setJarFile(String jarFile) { + myJarFile = jarFile; + return this; + } + + public boolean isStrict() { + return myIsStrict; + } + + public PatchSpec setStrict(boolean strict) { + myIsStrict = strict; + return this; + } + + public List getIgnoredFiles() { + return myIgnoredFiles; + } + + public PatchSpec setIgnoredFiles(List ignoredFiles) { + myIgnoredFiles = ignoredFiles; + return this; + } + + public List getCriticalFiles() { + return myCriticalFiles; + } + + public PatchSpec setCriticalFiles(List criticalFiles) { + myCriticalFiles = criticalFiles; + return this; + } + + public List getOptionalFiles() { + return myOptionalFiles; + } + + public PatchSpec setOptionalFiles(List optionalFiles) { + myOptionalFiles = optionalFiles; + return this; + } + + public PatchSpec setBinary(boolean binary) { + myIsBinary = binary; + return this; + } + + public boolean isBinary() { + return myIsBinary; + } + + public boolean isNormalized() { + return myIsNormalized; + } + + public PatchSpec setNormalized(boolean normalized) { + myIsNormalized = normalized; + return this; + } + + public PatchSpec setWarnings(Map warnings) { + myWarnings = warnings; + return this; + } + + public Map getWarnings() { + return myWarnings; + } + + public PatchSpec setDeleteFiles(List deleteFiles) { + myDeleteFiles = deleteFiles; + return this; + } + + public List getDeleteFiles() { + return myDeleteFiles; + } + + public PatchSpec setRoot(String root) { + myRoot = root; + return this; + } + + public String getRoot() { + return myRoot; + } +} diff --git a/updater/src/com/intellij/updater/RetryException.java b/updater/src/com/intellij/updater/RetryException.java new file mode 100644 index 000000000000..e13c4eb0f2c8 --- /dev/null +++ b/updater/src/com/intellij/updater/RetryException.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.updater; + +import java.io.IOException; + +/** + * Exception thrown when an IOException arises when performing a patch + * action and it's likely that retrying will be successful. + */ +public class RetryException extends IOException { + public RetryException() { + } + + public RetryException(String message) { + super(message); + } + + public RetryException(String message, Throwable cause) { + super(message, cause); + } + + public RetryException(Throwable cause) { + super(cause); + } +} diff --git a/updater/src/com/intellij/updater/Runner.java b/updater/src/com/intellij/updater/Runner.java index 354dba99713a..1d5d9c0217d2 100644 --- a/updater/src/com/intellij/updater/Runner.java +++ b/updater/src/com/intellij/updater/Runner.java @@ -18,19 +18,13 @@ import java.util.zip.ZipInputStream; public class Runner { public static Logger logger = null; - /** - * Treats zip files as regular binary files. When false, zip/jar files are unzipped and diffed file by file. - * When true, the entire zip file is diffed as a single file. Set to true if preserving the timestamps of - * the files inside the zip is important. This variable can change via a command line option. - */ - public static boolean ZIP_AS_BINARY = false; - private static final String PATCH_FILE_NAME = "patch-file.zip"; - private static final String PATCH_PROPERTIES_ENTRY = "patch.properties"; - private static final String OLD_BUILD_DESCRIPTION = "old.build.description"; - private static final String NEW_BUILD_DESCRIPTION = "new.build.description"; public static void main(String[] args) throws Exception { + + String jarFile = getArgument(args, "jar"); + jarFile = jarFile == null ? resolveJarFile() : jarFile; + if (args.length >= 6 && "create".equals(args[0])) { String oldVersionDesc = args[1]; String newVersionDesc = args[2]; @@ -39,25 +33,64 @@ public class Runner { String patchFile = args[5]; initLogger(); - ZIP_AS_BINARY = Arrays.asList(args).contains("--zip_as_binary"); + // See usage for an explanation of these flags + boolean binary = Arrays.asList(args).contains("--zip_as_binary"); + boolean strict = Arrays.asList(args).contains("--strict"); + boolean normalized = Arrays.asList(args).contains("--normalized"); - List ignoredFiles = extractFiles(args, "ignored"); - List criticalFiles = extractFiles(args, "critical"); - List optionalFiles = extractFiles(args, "optional"); - create(oldVersionDesc, newVersionDesc, oldFolder, newFolder, patchFile, ignoredFiles, criticalFiles, optionalFiles); + String root = getArgument(args, "root"); + root = root == null ? "" : (root.endsWith("/") ? root : root + "/"); + + List ignoredFiles = extractArguments(args, "ignored"); + List criticalFiles = extractArguments(args, "critical"); + List optionalFiles = extractArguments(args, "optional"); + List deleteFiles = extractArguments(args, "delete"); + Map warnings = buildWarningMap(extractArguments(args, "warning")); + + PatchSpec spec = new PatchSpec() + .setOldVersionDescription(oldVersionDesc) + .setNewVersionDescription(newVersionDesc) + .setRoot(root) + .setOldFolder(oldFolder) + .setNewFolder(newFolder) + .setPatchFile(patchFile) + .setJarFile(jarFile) + .setStrict(strict) + .setBinary(binary) + .setNormalized(normalized) + .setIgnoredFiles(ignoredFiles) + .setCriticalFiles(criticalFiles) + .setOptionalFiles(optionalFiles) + .setDeleteFiles(deleteFiles) + .setWarnings(warnings); + + create(spec); } else if (args.length >= 2 && "install".equals(args[0])) { String destFolder = args[1]; initLogger(); logger.info("destFolder: " + destFolder); - install(destFolder); + install(jarFile, destFolder); } else { printUsage(); } } + private static Map buildWarningMap(List warnings) { + Map map = new HashMap(); + for (String warning : warnings) { + int ix = warning.indexOf(":"); + if (ix != -1) { + String path = warning.substring(0, ix); + String message = warning.substring(ix + 1).replace("\\n","\n"); + map.put(path, message); + } + } + return map; + } + // checks that log directory 1)exists 2)has write perm. and 3)has 1MB+ free space private static boolean isValidLogDir(String logFolder) { File fileLogDir = new File(logFolder); @@ -106,7 +139,17 @@ public class Runner { logger.error(e.getMessage(), e); } - public static List extractFiles(String[] args, String paramName) { + public static String getArgument(String[] args, String name) { + String flag = "--" + name + "="; + for (String param : args) { + if (param.startsWith(flag)) { + return param.substring(flag.length()); + } + } + return null; + } + + public static List extractArguments(String[] args, String paramName) { List result = new ArrayList(); for (String param : args) { if (param.startsWith(paramName + "=")) { @@ -122,38 +165,51 @@ public class Runner { @SuppressWarnings("UseOfSystemOutOrSystemErr") private static void printUsage() { - System.err.println("Usage:\n" + - "create " + - " [ignored=file1;file2;...] [critical=file1;file2;...] [optional=file1;file2;...]\n" + - "install \n"); + System.err.println( + "Usage:\n" + + " Runner create [=file1;file2;...] []\n" + + " Runner install \n" + + "\n" + + "Where:\n" + + " : A description of the version to generate the patch from.\n" + + " : A description of the version to generate the patch to.\n" + + " : The folder where to find the old version.\n" + + " : The folder where to find the new version.\n" + + " : The .jar patch file to create which contains the patch and the patcher.\n" + + " : Can be one of:\n" + + " ignored: The set of files that will not be included in the patch.\n" + + " critical: Fully included in the patch, so they can be replaced at destination even if they have changed.\n" + + " optional: A set of files that is ok for them no to exist when applying the patch.\n" + + " delete: A set of regular expressions for paths that is safe to delete without user confirmation.\n" + + " : Can be:\n" + + " --zip_as_binary: Zip and jar files will be treated as binary files and not inspected internally.\n" + + " --strict: The created patch will contain extra information to fully validate an installation. A strict\n" + + " patch will only be applied if it is guaranteed that the patched version will match exactly\n" + + " the source of the patch. This means that unexpected files will be deleted and all existing files\n" + + " will be validated\n" + + " --root=: Sets dir as the root directory of the patch. The root directory is the directory where the patch should be" + + " applied to. For example on Mac, you can diff the two .app folders and set Contents as the root." + + " The root directory is relative to and uses forwards-slashes as separators." + + " --normalized: This creates a normalized patch. This flag only makes sense in addition to --zip_as_binary\n" + + " A normalized patch must be used to move from an installation that was patched\n" + + " in a non-binary way to a fully binary patch. This will yield a larger patch, but the\n" + + " generated patch can be applied on versions where non-binary patches have been applied to and it\n" + + " guarantees that the patched version will match exactly the original one.\n"); } - private static void create(String oldBuildDesc, - String newBuildDesc, - String oldFolder, - String newFolder, - String patchFile, - List ignoredFiles, - List criticalFiles, - List optionalFiles) throws IOException, OperationCancelledException { + private static void create(PatchSpec spec) throws IOException, OperationCancelledException { UpdaterUI ui = new ConsoleUpdaterUI(); try { File tempPatchFile = Utils.createTempFile(); - PatchFileCreator.create(new File(oldFolder), - new File(newFolder), - tempPatchFile, - ignoredFiles, - criticalFiles, - optionalFiles, - ui); + PatchFileCreator.create(spec, tempPatchFile, ui); - logger.info("Packing JAR file: " + patchFile ); - ui.startProcess("Packing JAR file '" + patchFile + "'..."); + logger.info("Packing JAR file: " + spec.getPatchFile() ); + ui.startProcess("Packing JAR file '" + spec.getPatchFile() + "'..."); - FileOutputStream fileOut = new FileOutputStream(patchFile); + FileOutputStream fileOut = new FileOutputStream(spec.getPatchFile()); try { ZipOutputWrapper out = new ZipOutputWrapper(fileOut); - ZipInputStream in = new ZipInputStream(new FileInputStream(resolveJarFile())); + ZipInputStream in = new ZipInputStream(new FileInputStream(new File(spec.getJarFile()))); try { ZipEntry e; while ((e = in.getNextEntry()) != null) { @@ -164,18 +220,6 @@ public class Runner { in.close(); } - ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); - try { - Properties props = new Properties(); - props.setProperty(OLD_BUILD_DESCRIPTION, oldBuildDesc); - props.setProperty(NEW_BUILD_DESCRIPTION, newBuildDesc); - props.store(byteOut, ""); - } - finally { - byteOut.close(); - } - - out.zipBytes(PATCH_PROPERTIES_ENTRY, byteOut); out.zipFile(PATCH_FILE_NAME, tempPatchFile); out.finish(); } @@ -195,18 +239,7 @@ public class Runner { Utils.cleanup(); } - private static void install(final String destFolder) throws Exception { - InputStream in = Runner.class.getResourceAsStream("/" + PATCH_PROPERTIES_ENTRY); - Properties props = new Properties(); - if (in != null) { - try { - props.load(in); - } - finally { - in.close(); - } - } - + private static void install(final String jarFile, final String destFolder) throws Exception { // todo[r.sh] to delete in IDEA 14 (after a full circle of platform updates) if (System.getProperty("swing.defaultlaf") == null) { SwingUtilities.invokeAndWait(new Runnable() { @@ -222,27 +255,25 @@ public class Runner { }); } - new SwingUpdaterUI(props.getProperty(OLD_BUILD_DESCRIPTION), - props.getProperty(NEW_BUILD_DESCRIPTION), - new SwingUpdaterUI.InstallOperation() { - public boolean execute(UpdaterUI ui) throws OperationCancelledException { - logger.info("installing patch to the " + destFolder); - return doInstall(ui, destFolder); - } - }); + new SwingUpdaterUI(new SwingUpdaterUI.InstallOperation() { + public boolean execute(UpdaterUI ui) throws OperationCancelledException { + logger.info("installing patch to the " + destFolder); + return doInstall(jarFile, ui, destFolder); + } + }); } - private static boolean doInstall(UpdaterUI ui, String destFolder) throws OperationCancelledException { + private static boolean doInstall(String jarFile, UpdaterUI ui, String destFolder) throws OperationCancelledException { try { try { File patchFile = Utils.createTempFile(); - ZipFile jarFile = new ZipFile(resolveJarFile()); + ZipFile zipFile = new ZipFile(jarFile); logger.info("Extracting patch file..."); ui.startProcess("Extracting patch file..."); ui.setProgressIndeterminate(); try { - InputStream in = Utils.getEntryInputStream(jarFile, PATCH_FILE_NAME); + InputStream in = Utils.getEntryInputStream(zipFile, PATCH_FILE_NAME); OutputStream out = new BufferedOutputStream(new FileOutputStream(patchFile)); try { Utils.copyStream(in, out); @@ -253,7 +284,7 @@ public class Runner { } } finally { - jarFile.close(); + zipFile.close(); } ui.checkCancelled(); @@ -270,6 +301,7 @@ public class Runner { } finally { try { + System.gc(); cleanup(ui); } catch (IOException e) { @@ -281,11 +313,7 @@ public class Runner { return false; } - private static File resolveJarFile() throws IOException { - String jar = System.getProperty("JAR_FILE"); - if (jar != null) { - return new File(jar); - } + private static String resolveJarFile() throws IOException { URL url = Runner.class.getResource(""); if (url == null) throw new IOException("Cannot resolve JAR file path"); if (!"jar".equals(url.getProtocol())) throw new IOException("Patch file is not a JAR file"); @@ -299,7 +327,7 @@ public class Runner { String jarFileUrl = path.substring(start, end); try { - return new File(new URI(jarFileUrl)); + return new File(new URI(jarFileUrl)).getAbsolutePath(); } catch (URISyntaxException e) { printStackTrace(e); diff --git a/updater/src/com/intellij/updater/SwingUpdaterUI.java b/updater/src/com/intellij/updater/SwingUpdaterUI.java index d44fc2e094f1..88f8ff96fc73 100644 --- a/updater/src/com/intellij/updater/SwingUpdaterUI.java +++ b/updater/src/com/intellij/updater/SwingUpdaterUI.java @@ -49,7 +49,7 @@ public class SwingUpdaterUI implements UpdaterUI { private final JFrame myFrame; private boolean myApplied; - public SwingUpdaterUI(String oldBuildDesc, String newBuildDesc, InstallOperation operation) { + public SwingUpdaterUI(InstallOperation operation) { myOperation = operation; myProcessTitle = new JLabel(" "); @@ -106,8 +106,6 @@ public class SwingUpdaterUI implements UpdaterUI { buttonsPanel.add(Box.createHorizontalGlue()); buttonsPanel.add(myCancelButton); - myProcessTitle.setText("Updating " + oldBuildDesc + " to " + newBuildDesc + "..."); - myFrame.add(processPanel, BorderLayout.CENTER); myFrame.add(buttonsPanel, BorderLayout.SOUTH); @@ -127,6 +125,18 @@ public class SwingUpdaterUI implements UpdaterUI { startRequestDispatching(); } + @Override + public void setDescription(String oldBuildDesc, String newBuildDesc) { + myProcessTitle.setText("Updating " + oldBuildDesc + " to " + newBuildDesc + "..."); + } + + @Override + public boolean showWarning(String message) { + Object[] choices = new Object[] { "Retry", "Exit" }; + int choice = JOptionPane.showOptionDialog(null, message, "Warning", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, choices, choices[0]); + return choice == 0; + } + private void startRequestDispatching() { new Thread(new Runnable() { public void run() { @@ -214,6 +224,14 @@ public class SwingUpdaterUI implements UpdaterUI { try { SwingUtilities.invokeAndWait(new Runnable() { public void run() { + boolean proceed = true; + for (ValidationResult result : validationResults) { + if (result.options.contains(ValidationResult.Option.NONE)) { + proceed = false; + break; + } + } + final JDialog dialog = new JDialog(myFrame, TITLE, true); dialog.setLayout(new BorderLayout()); dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); @@ -222,12 +240,6 @@ public class SwingUpdaterUI implements UpdaterUI { buttonsPanel.setBorder(BUTTONS_BORDER); buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.X_AXIS)); buttonsPanel.add(Box.createHorizontalGlue()); - JButton proceedButton = new JButton(PROCEED_BUTTON_TITLE); - proceedButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - dialog.setVisible(false); - } - }); JButton cancelButton = new JButton(CANCEL_BUTTON_TITLE); cancelButton.addActionListener(new ActionListener() { @@ -237,11 +249,20 @@ public class SwingUpdaterUI implements UpdaterUI { dialog.setVisible(false); } }); - - buttonsPanel.add(proceedButton); buttonsPanel.add(cancelButton); - dialog.getRootPane().setDefaultButton(proceedButton); + if (proceed) { + JButton proceedButton = new JButton(PROCEED_BUTTON_TITLE); + proceedButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + dialog.setVisible(false); + } + }); + buttonsPanel.add(proceedButton); + dialog.getRootPane().setDefaultButton(proceedButton); + } else { + dialog.getRootPane().setDefaultButton(cancelButton); + } JTable table = new JTable(); @@ -256,10 +277,16 @@ public class SwingUpdaterUI implements UpdaterUI { each.setPreferredWidth(MyTableModel.getColumnWidth(i, new Dimension(600, 400).width)); } - String message = "There are some conflicts found in the installation.

" + - "Please select desired solutions from the " + MyTableModel.COLUMNS[MyTableModel.OPTIONS_COLUMN_INDEX] + - " column and press " + PROCEED_BUTTON_TITLE + ".
" + - "If you do not want to proceed with the update, please press " + CANCEL_BUTTON_TITLE + "."; + String message = "Some conflicts were found in the installation area.

"; + + if (proceed) { + message += "Please select desired solutions from the " + MyTableModel.COLUMNS[MyTableModel.OPTIONS_COLUMN_INDEX] + + " column and press " + PROCEED_BUTTON_TITLE + ".
" + + "If you do not want to proceed with the update, please press " + CANCEL_BUTTON_TITLE + "."; + } else { + message += "Some of the conflicts below do not have a solution, so the patch cannot be applied.
" + + "Press " + CANCEL_BUTTON_TITLE + " to exit."; + } JLabel label = new JLabel(message); label.setBorder(LABEL_BORDER); @@ -354,7 +381,7 @@ public class SwingUpdaterUI implements UpdaterUI { } public static void main(String[] args) { - new SwingUpdaterUI("xxx", "yyy", new InstallOperation() { + new SwingUpdaterUI(new InstallOperation() { public boolean execute(UpdaterUI ui) throws OperationCancelledException { ui.startProcess("Process1"); ui.checkCancelled(); diff --git a/updater/src/com/intellij/updater/UpdateAction.java b/updater/src/com/intellij/updater/UpdateAction.java index 08cf7a188831..b9c34d8413b7 100644 --- a/updater/src/com/intellij/updater/UpdateAction.java +++ b/updater/src/com/intellij/updater/UpdateAction.java @@ -6,48 +6,54 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class UpdateAction extends BaseUpdateAction { - public UpdateAction(String path, long checksum) { - super(path, checksum); + public UpdateAction(Patch patch, String path, String source, long checksum, boolean move) { + super(patch, path, source, checksum, move); } - public UpdateAction(DataInputStream in) throws IOException { - super(in); + public UpdateAction(Patch patch, String path, long checksum) { + this(patch, path, path, checksum, false); + } + + public UpdateAction(Patch patch, DataInputStream in) throws IOException { + super(patch, in); } @Override protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { - patchOutput.putNextEntry(new ZipEntry(myPath)); - writeExecutableFlag(patchOutput, newerFile); - writeDiff(olderFile, newerFile, patchOutput); - patchOutput.closeEntry(); + if (!myIsMove) { + patchOutput.putNextEntry(new ZipEntry(myPath)); + writeExecutableFlag(patchOutput, newerFile); + writeDiff(olderFile, newerFile, patchOutput); + patchOutput.closeEntry(); + } } @Override - protected boolean isModified(File toFile) throws IOException { - return myChecksum != Digester.digestRegularFile(toFile); - } + protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException { + File source = getSource(backupDir); + File updated; + if (!myIsMove) { + updated = Utils.createTempFile(); + InputStream in = Utils.findEntryInputStream(patchFile, myPath); + boolean executable = readExecutableFlag(in); - @Override - protected void doApply(ZipFile patchFile, File toFile) throws IOException { - InputStream in = Utils.findEntryInputStream(patchFile, myPath); - boolean executable = readExecutableFlag(in); - - File temp = Utils.createTempFile(); - OutputStream out = new BufferedOutputStream(new FileOutputStream(temp)); - try { - InputStream oldFileIn = new FileInputStream(toFile); + OutputStream out = new BufferedOutputStream(new FileOutputStream(updated)); try { - applyDiff(in, oldFileIn, out); + InputStream oldFileIn = Utils.newFileInputStream(source, myPatch.isNormalized()); + try { + applyDiff(in, oldFileIn, out); + } + finally { + oldFileIn.close(); + } } finally { - oldFileIn.close(); + out.close(); } + Utils.setExecutable(updated, executable); + } else { + updated = source; } - finally { - out.close(); - } - - replaceUpdated(temp, toFile); - Utils.setExecutable(toFile, executable); + replaceUpdated(updated, toFile); } } diff --git a/updater/src/com/intellij/updater/UpdateZipAction.java b/updater/src/com/intellij/updater/UpdateZipAction.java index d808a735f0cd..2c8e78eb14b5 100644 --- a/updater/src/com/intellij/updater/UpdateZipAction.java +++ b/updater/src/com/intellij/updater/UpdateZipAction.java @@ -12,24 +12,24 @@ public class UpdateZipAction extends BaseUpdateAction { Set myFilesToUpdate; Set myFilesToDelete; - public UpdateZipAction(String path, long checksum) { - super(path, checksum); + public UpdateZipAction(Patch patch, String path, String source, long checksum, boolean move) { + super(patch, path, source, checksum, move); } // test support - public UpdateZipAction(String path, + public UpdateZipAction(Patch patch, String path, Collection filesToCreate, Collection filesToUpdate, Collection filesToDelete, long checksum) { - super(path, checksum); + super(patch, path, path, checksum, false); myFilesToCreate = new HashSet(filesToCreate); myFilesToUpdate = new HashSet(filesToUpdate); myFilesToDelete = new HashSet(filesToDelete); } - public UpdateZipAction(DataInputStream in) throws IOException { - super(in); + public UpdateZipAction(Patch patch, DataInputStream in) throws IOException { + super(patch, in); int count = in.readInt(); myFilesToCreate = new HashSet(count); @@ -87,9 +87,9 @@ public class UpdateZipAction extends BaseUpdateAction { } }); - DiffCalculator.Result diff = DiffCalculator.calculate(oldCheckSums, newCheckSums); + DiffCalculator.Result diff = DiffCalculator.calculate(oldCheckSums, newCheckSums, new LinkedList(), false); - myFilesToCreate = diff.filesToCreate; + myFilesToCreate = diff.filesToCreate.keySet(); myFilesToUpdate = diff.filesToUpdate.keySet(); myFilesToDelete = diff.filesToDelete.keySet(); @@ -154,18 +154,15 @@ public class UpdateZipAction extends BaseUpdateAction { } @Override - protected boolean isModified(File toFile) throws IOException { - return myChecksum != Digester.digestFile(toFile); - } - - protected void doApply(final ZipFile patchFile, File toFile) throws IOException { + protected void doApply(final ZipFile patchFile, File backupDir, File toFile) throws IOException { File temp = Utils.createTempFile(); FileOutputStream fileOut = new FileOutputStream(temp); try { final ZipOutputWrapper out = new ZipOutputWrapper(fileOut); out.setCompressionLevel(0); - processZipFile(toFile, new Processor() { + processZipFile(getSource(backupDir), new Processor() { + @Override public void process(ZipEntry entry, InputStream in) throws IOException { String path = entry.getName(); if (myFilesToDelete.contains(path)) return; diff --git a/updater/src/com/intellij/updater/UpdaterUI.java b/updater/src/com/intellij/updater/UpdaterUI.java index 85005ff81205..4a38e0fea3ec 100644 --- a/updater/src/com/intellij/updater/UpdaterUI.java +++ b/updater/src/com/intellij/updater/UpdaterUI.java @@ -16,5 +16,15 @@ public interface UpdaterUI { void checkCancelled() throws OperationCancelledException; + void setDescription(String oldBuildDesc, String newBuildDesc); + + /** + * Shows a warning associated with the pretense of a file and asks the user if the validation needs be retried. + * This function will return true iff the user wants to retry. + * @param message The warning message to display. + * @return true if the validation needs to be retried or false if te updater should quit. + */ + boolean showWarning(String message); + Map askUser(List validationResults) throws OperationCancelledException; } \ No newline at end of file diff --git a/updater/src/com/intellij/updater/Utils.java b/updater/src/com/intellij/updater/Utils.java index bce3333e106e..b9ac29a46348 100644 --- a/updater/src/com/intellij/updater/Utils.java +++ b/updater/src/com/intellij/updater/Utils.java @@ -1,7 +1,7 @@ package com.intellij.updater; import java.io.*; -import java.util.LinkedHashSet; +import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -73,6 +73,7 @@ public class Utils { public static void copy(File from, File to) throws IOException { Runner.logger.info("from " + from.getPath() + " to " + to.getPath()); if (from.isDirectory()) { + to.mkdirs(); File[] files = from.listFiles(); if (files == null) throw new IOException("Cannot get directory's content: " + from); for (File each : files) { @@ -91,6 +92,15 @@ public class Utils { } } + + public static void mirror(File from, File to) throws IOException { + if (from.exists()) { + copy(from, to); + } else { + delete(to); + } + } + public static void copyFileToStream(File from, OutputStream out) throws IOException { InputStream in = new BufferedInputStream(new FileInputStream(from)); try { @@ -146,40 +156,121 @@ public class Utils { } public static InputStream getEntryInputStream(ZipFile zipFile, String entryPath) throws IOException { - InputStream result = findEntryInputStream(zipFile, entryPath); - if (result == null) throw new IOException("Entry " + entryPath + " not found"); - Runner.logger.info("entryPath: " + entryPath); - return result; + ZipEntry entry = getZipEntry(zipFile, entryPath); + return findEntryInputStreamForEntry(zipFile, entry); } public static InputStream findEntryInputStream(ZipFile zipFile, String entryPath) throws IOException { ZipEntry entry = zipFile.getEntry(entryPath); - if (entry == null || entry.isDirectory()) return null; + if (entry == null) return null; + return findEntryInputStreamForEntry(zipFile, entry); + } - // if isDirectory check failed, check presence of 'file/' manually - if (!entryPath.endsWith("/") && zipFile.getEntry(entryPath + "/") != null) return null; + public static ZipEntry getZipEntry(ZipFile zipFile, String entryPath) throws IOException { + ZipEntry entry = zipFile.getEntry(entryPath); + if (entry == null) throw new IOException("Entry " + entryPath + " not found"); + Runner.logger.info("entryPath: " + entryPath); + return entry; + } + + public static InputStream findEntryInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException { + if (entry.isDirectory()) return null; + // There is a bug in some JVM implementations where for a directory "X/" in a zipfile, if we do + // "zip.getEntry("X/").isDirectory()" returns true, but if we do "zip.getEntry("X").isDirectory()" is false. + // getEntry for "name" falls back to finding "X/", so here we make sure that didn't happen. + if (zipFile.getEntry(entry.getName() + "/") != null) return null; return new BufferedInputStream(zipFile.getInputStream(entry)); } - public static LinkedHashSet collectRelativePaths(File dir) { + public static LinkedHashSet collectRelativePaths(File dir, boolean includeDirectories) { LinkedHashSet result = new LinkedHashSet(); - collectRelativePaths(dir, result, null); + collectRelativePaths(dir, result, null, includeDirectories); return result; } - private static void collectRelativePaths(File dir, LinkedHashSet result, String parentPath) { + private static void collectRelativePaths(File dir, LinkedHashSet result, String parentPath, boolean includeDirectories) { File[] children = dir.listFiles(); if (children == null) return; for (File each : children) { String relativePath = (parentPath == null ? "" : parentPath + "/") + each.getName(); if (each.isDirectory()) { - collectRelativePaths(each, result, relativePath); + if (includeDirectories) { + // The trailing slash is very important, as it's used by zip to determine whether it is a directory. + result.add(relativePath + "/"); + } + collectRelativePaths(each, result, relativePath, includeDirectories); } else { result.add(relativePath); } } } + + public static InputStream newFileInputStream(File file, boolean normalize) throws IOException { + if (!normalize || !isZipFile(file.getName())) { + return new FileInputStream(file); + } + return new NormalizedZipInputStream(file); + } + + static class NormalizedZipInputStream extends InputStream { + + private ArrayList myEntries; + private InputStream myStream = null; + private int myNextEntry = 0; + private final ZipFile myZip; + private byte[] myByte = new byte[1]; + + NormalizedZipInputStream(File file) throws IOException { + myZip = new ZipFile(file); + myEntries = Collections.list(myZip.entries()); + Collections.sort(myEntries, new Comparator() { + @Override + public int compare(ZipEntry a, ZipEntry b) { + return a.getName().compareTo(b.getName()); + } + }); + + loadNextEntry(); + } + + private void loadNextEntry() throws IOException { + if (myStream != null) { + myStream.close(); + } + myStream = null; + while (myNextEntry < myEntries.size() && myStream == null) { + myStream = findEntryInputStreamForEntry(myZip, myEntries.get(myNextEntry++)); + } + } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + if (myStream == null) { + return -1; + } + int b = myStream.read(bytes, off, len); + if (b == -1) { + loadNextEntry(); + return read(bytes, off, len); + } + return b; + } + + @Override + public int read() throws IOException { + int b = read(myByte, 0, 1); + return b == -1 ? -1 : myByte[0]; + } + + @Override + public void close() throws IOException { + if (myStream != null) { + myStream.close(); + } + myZip.close(); + } + } } diff --git a/updater/src/com/intellij/updater/ValidateAction.java b/updater/src/com/intellij/updater/ValidateAction.java new file mode 100644 index 000000000000..a9ae8eb59965 --- /dev/null +++ b/updater/src/com/intellij/updater/ValidateAction.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.updater; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +public class ValidateAction extends PatchAction { + // Only used on patch creation + protected transient File myOlderDir; + + public ValidateAction(Patch patch, String path, long checksum) { + super(patch, path, checksum); + } + + public ValidateAction(Patch patch, DataInputStream in) throws IOException { + super(patch, in); + } + + @Override + protected void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException { + } + + @Override + public ValidationResult validate(File toDir) throws IOException { + return doValidateNotChanged(getFile(toDir), ValidationResult.Kind.ERROR, ValidationResult.Action.VALIDATE); + } + + @Override + protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException { + } + + @Override + protected void doBackup(File toFile, File backupFile) throws IOException { + } + + @Override + protected void doRevert(File toFile, File backupFile) throws IOException { + } +} diff --git a/updater/src/com/intellij/updater/ValidationResult.java b/updater/src/com/intellij/updater/ValidationResult.java index 6f56fd3bc0c3..dc5fcd40b56d 100644 --- a/updater/src/com/intellij/updater/ValidationResult.java +++ b/updater/src/com/intellij/updater/ValidationResult.java @@ -9,7 +9,7 @@ public class ValidationResult implements Comparable { } public enum Action { - CREATE("Create"), UPDATE("Update"), DELETE("Delete"), NO_ACTION(""); + CREATE("Create"), UPDATE("Update"), DELETE("Delete"), NO_ACTION(""), VALIDATE("Validate"); private final String myDisplayString; @@ -24,7 +24,7 @@ public class ValidationResult implements Comparable { } public enum Option { - IGNORE, KEEP, REPLACE, DELETE, KILL_PROCESS + NONE, IGNORE, KEEP, REPLACE, DELETE, KILL_PROCESS } public static final String ABSENT_MESSAGE = "Absent"; diff --git a/updater/testData/Readme.txt b/updater/testData/Readme.txt index 3b5efe459ee0..c509af0d5f17 100644 --- a/updater/testData/Readme.txt +++ b/updater/testData/Readme.txt @@ -71,13 +71,13 @@ Uninstalling IntelliJ IDEA Licensing & pricing ========================== - Licensing and pricing information can be found at https://www.jetbrains.com/idea/buy/index.html. + Licensing and pricing information can be found at http://www.jetbrains.com/idea/buy/index.html. IntelliJ IDEA Overview ========================== For general info and facts on IntelliJ IDEA, you can refer to IntelliJ IDEA Info Kit at - https://www.jetbrains.com/idea/documentation/product_info_kit.html. + http://www.jetbrains.com/idea/documentation/product_info_kit.html. IDEA Development Package @@ -102,7 +102,7 @@ IDEA Development Package * StarTeam, Perforce, Subversion, Visual SourceSafe integration * Tomcat, Weblogic, WebSphere, Geronimo, JBoss, GlassFish, JSR45 integration - Download page: https://www.jetbrains.com/idea/download/index.html + Download page: http://www.jetbrains.com/idea/download/index.html Source code of additional open source plugins shipped with IntelliJ IDEA is available in the Subversion repository at: @@ -139,7 +139,7 @@ IntelliJ Community Site Support ======= For technical support and assistance, you may find necessary information at the Support page - (https://www.jetbrains.com/support/index.html) or contact us at support@jetbrains.com. + (http://www.jetbrains.com/support/index.html) or contact us at support@jetbrains.com. Bug Reporting: @@ -158,7 +158,7 @@ Contacting us: ============= -You are encouraged to visit our IntelliJ IDEA web site at https://www.jetbrains.com/idea/ +You are encouraged to visit our IntelliJ IDEA web site at http://www.jetbrains.com/idea/ or to contact us via e-mail at feedback@jetbrains.com if you have any comments about this release. In particular, we are very interested in any ease-of-use, user interface suggestions that you may have. We will be posting regular updates diff --git a/updater/testSrc/com/intellij/updater/DigesterTest.java b/updater/testSrc/com/intellij/updater/DigesterTest.java index 3b524f8288e9..885db6be7b1e 100644 --- a/updater/testSrc/com/intellij/updater/DigesterTest.java +++ b/updater/testSrc/com/intellij/updater/DigesterTest.java @@ -2,20 +2,33 @@ package com.intellij.updater; import org.junit.Test; -import java.util.Collections; -import java.util.Map; +import java.io.File; import static org.junit.Assert.assertEquals; public class DigesterTest extends UpdaterTestCase { @Test public void testBasics() throws Exception { - Runner.initLogger(); - Map checkSums = Digester.digestFiles(getDataDir(), Collections.emptyList(), TEST_UI); - assertEquals(12, checkSums.size()); + assertEquals(CHECKSUMS.README_TXT, Digester.digestRegularFile(new File(getDataDir(), "Readme.txt"), false)); + assertEquals(CHECKSUMS.FOCUSKILLER_DLL, Digester.digestRegularFile(new File(getDataDir(), "/bin/focuskiller.dll"), false)); + assertEquals(CHECKSUMS.BOOTSTRAP_JAR, Digester.digestZipFile(new File(getDataDir(), "/lib/bootstrap.jar"))); + assertEquals(CHECKSUMS.BOOTSTRAP_JAR_BINARY, Digester.digestRegularFile(new File(getDataDir(), "/lib/bootstrap.jar"), false)); - assertEquals(CHECKSUMS.README_TXT, (long)checkSums.get("Readme.txt")); - assertEquals(CHECKSUMS.FOCUSKILLER_DLL, (long)checkSums.get("bin/focuskiller.dll")); - assertEquals(CHECKSUMS.BOOTSTRAP_JAR, (long)checkSums.get("lib/bootstrap.jar")); + assertEquals(CHECKSUMS.ANNOTATIONS_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/annotations.jar"), true)); + assertEquals(CHECKSUMS.ANNOTATIONS_CHANGED_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/annotations_changed.jar"), true)); + assertEquals(CHECKSUMS.BOOT_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/boot.jar"), true)); + assertEquals(CHECKSUMS.BOOT2_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/boot2.jar"), true)); + assertEquals(CHECKSUMS.BOOT2_CHANGED_WITH_UNCHANGED_CONTENT_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/boot2_changed_with_unchanged_content.jar"), true)); + assertEquals(CHECKSUMS.BOOT_WITH_DIRECTORY_BECOMES_FILE_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/boot_with_directory_becomes_file.jar"), true)); + assertEquals(CHECKSUMS.BOOTSTRAP_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/bootstrap.jar"), true)); + assertEquals(CHECKSUMS.BOOTSTRAP_DELETED_JAR_NORM, + Digester.digestRegularFile(new File(getDataDir(), "/lib/bootstrap_deleted.jar"), true)); } } \ No newline at end of file diff --git a/updater/testSrc/com/intellij/updater/PatchFileCreatorBinaryTest.java b/updater/testSrc/com/intellij/updater/PatchFileCreatorBinaryTest.java new file mode 100644 index 000000000000..6cacfe93a6ef --- /dev/null +++ b/updater/testSrc/com/intellij/updater/PatchFileCreatorBinaryTest.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.updater; + +public class PatchFileCreatorBinaryTest extends PatchFileCreatorTest { + @Override + public void setUp() throws Exception { + super.setUp(); + myPatchSpec.setBinary(true); + } +} diff --git a/updater/testSrc/com/intellij/updater/PatchFileCreatorNotBinaryTest.java b/updater/testSrc/com/intellij/updater/PatchFileCreatorNotBinaryTest.java new file mode 100644 index 000000000000..666dbc2366d6 --- /dev/null +++ b/updater/testSrc/com/intellij/updater/PatchFileCreatorNotBinaryTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.updater; + +import com.intellij.openapi.util.io.FileUtil; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PatchFileCreatorNotBinaryTest extends PatchFileCreatorTest { + @Override + public void setUp() throws Exception { + super.setUp(); + } + + @Test + public void failOnEmptyTargetJar() throws Exception { + final File sourceJar = new File(myOlderDir, "lib/empty.jar"); + FileUtil.copy(new File(myOlderDir, "lib/annotations.jar"), sourceJar); + + try { + final File targetJar = new File(myNewerDir, "lib/empty.jar"); + if (targetJar.exists()) targetJar.delete(); + assertTrue(targetJar.createNewFile()); + + try { + createPatch(); + fail("Should have failed to create a patch against empty .jar"); + } + catch (IOException e) { + final String reason = e.getMessage(); + assertEquals("Corrupted target file: " + targetJar, reason); + } + finally { + targetJar.delete(); + } + } + finally { + sourceJar.delete(); + } + } + + @Test + public void failOnEmptySourceJar() throws Exception { + final File sourceJar = new File(myOlderDir, "lib/empty.jar"); + if (sourceJar.exists()) sourceJar.delete(); + assertTrue(sourceJar.createNewFile()); + + try { + final File targetJar = new File(myNewerDir, "lib/empty.jar"); + FileUtil.copy(new File(myNewerDir, "lib/annotations.jar"), targetJar); + + try { + createPatch(); + fail("Should have failed to create a patch from empty .jar"); + } + catch (IOException e) { + final String reason = e.getMessage(); + assertEquals("Corrupted source file: " + sourceJar, reason); + } + finally { + targetJar.delete(); + } + } + finally { + sourceJar.delete(); + } + } +} diff --git a/updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java b/updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java index 2cffb224c60f..76ecdbea5b19 100644 --- a/updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java +++ b/updater/testSrc/com/intellij/updater/PatchFileCreatorTest.java @@ -4,9 +4,7 @@ import com.intellij.openapi.util.io.FileUtil; import org.junit.Before; import org.junit.Test; -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; +import java.io.*; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -15,109 +13,89 @@ import java.util.zip.ZipOutputStream; import static org.junit.Assert.*; @SuppressWarnings("ResultOfMethodCallIgnored") -public class PatchFileCreatorTest extends PatchTestCase { +public abstract class PatchFileCreatorTest extends PatchTestCase { private File myFile; + protected PatchSpec myPatchSpec; @Override @Before public void setUp() throws Exception { super.setUp(); myFile = getTempFile("patch.zip"); + myPatchSpec = new PatchSpec() + .setOldFolder(myOlderDir.getAbsolutePath()) + .setNewFolder(myNewerDir.getAbsolutePath()); } @Test public void testCreatingAndApplying() throws Exception { - createPatch(); + Patch patch = createPatch(); - assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); } @Test - public void failOnEmptySourceJar() throws Exception { - final File sourceJar = new File(myOlderDir, "lib/empty.jar"); - if (sourceJar.exists()) sourceJar.delete(); - assertTrue(sourceJar.createNewFile()); - - try { - final File targetJar = new File(myNewerDir, "lib/empty.jar"); - FileUtil.copy(new File(myNewerDir, "lib/annotations.jar"), targetJar); - - try { - createPatch(); - fail("Should have failed to create a patch from empty .jar"); - } - catch (IOException e) { - final String reason = e.getMessage(); - assertEquals("Corrupted source file: " + sourceJar, reason); - } - finally { - targetJar.delete(); - } - } - finally { - sourceJar.delete(); - } + public void testCreatingAndApplyingStrict() throws Exception { + myPatchSpec.setStrict(true); + Patch patch = createPatch(); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); } @Test - public void failOnEmptyTargetJar() throws Exception { - final File sourceJar = new File(myOlderDir, "lib/empty.jar"); - FileUtil.copy(new File(myOlderDir, "lib/annotations.jar"), sourceJar); + public void testCreatingAndApplyingOnADifferentRoot() throws Exception { + myPatchSpec.setRoot("bin/"); + myPatchSpec.setStrict(true); - try { - final File targetJar = new File(myNewerDir, "lib/empty.jar"); - if (targetJar.exists()) targetJar.delete(); - assertTrue(targetJar.createNewFile()); + Patch patch = createPatch(); - try { - createPatch(); - fail("Should have failed to create a patch against empty .jar"); - } - catch (IOException e) { - final String reason = e.getMessage(); - assertEquals("Corrupted target file: " + targetJar, reason); - } - finally { - targetJar.delete(); - } - } - finally { - sourceJar.delete(); - } + File target = new File(myOlderDir, "bin"); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, target, TEST_UI)); + } + + @Test + public void testCreatingAndFailingOnADifferentRoot() throws Exception { + myPatchSpec.setRoot("bin/"); + myPatchSpec.setStrict(true); + + Patch patch = createPatch(); + + File target = new File(myOlderDir, "bin"); + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, target, TEST_UI); + preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch)); + assertNothingHasChanged(patch, preparationResult, new HashMap()); } @Test public void testReverting() throws Exception { - createPatch(); + Patch patch = createPatch(); PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); - preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction()); - assertNothingHasChanged(preparationResult, new HashMap()); + preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch)); + assertNothingHasChanged(patch, preparationResult, new HashMap()); } @Test public void testRevertedWhenFileToDeleteIsProcessLocked() throws Exception { if (!UtilsTest.mIsWindows) return; - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); RandomAccessFile raf = new RandomAccessFile(new File(myOlderDir, "bin/idea.bat"),"rw"); - // Lock the file. FileLock is not good here, because we need to prevent deletion. - int b = raf.read(); - raf.seek(0); - raf.write(b); - try { + // Lock the file. FileLock is not good here, because we need to prevent deletion. + int b = raf.read(); + raf.seek(0); + raf.write(b); + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); - Map original = Digester.digestFiles(myOlderDir, Collections.emptyList(), TEST_UI); + Map original = patch.digestFiles(myOlderDir, Collections.emptyList(), false, TEST_UI); File backup = getTempFile("backup"); PatchFileCreator.apply(preparationResult, new HashMap(), backup, TEST_UI); - assertEquals(original, Digester.digestFiles(myOlderDir, Collections.emptyList(), TEST_UI)); + assertEquals(original, patch.digestFiles(myOlderDir, Collections.emptyList(), false, TEST_UI)); } finally { raf.close(); @@ -126,44 +104,57 @@ public class PatchFileCreatorTest extends PatchTestCase { @Test public void testApplyingWithAbsentFileToDelete() throws Exception { - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); new File(myOlderDir, "bin/idea.bat").delete(); - assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + } + + @Test + public void testApplyingWithAbsentFileToUpdateStrict() throws Exception { + myPatchSpec.setStrict(true); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + new File(myOlderDir, "lib/annotations.jar").delete(); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertEquals(1, preparationResult.validationResults.size()); + assertEquals(new ValidationResult(ValidationResult.Kind.ERROR, + "lib/annotations.jar", + ValidationResult.Action.UPDATE, + ValidationResult.ABSENT_MESSAGE, + ValidationResult.Option.NONE), preparationResult.validationResults.get(0)); } @Test public void testApplyingWithAbsentOptionalFile() throws Exception { FileUtil.writeToFile(new File(myNewerDir, "bin/idea.bat"), "new content".getBytes()); - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.singletonList("bin/idea.bat"), TEST_UI); + myPatchSpec.setOptionalFiles(Collections.singletonList("bin/idea.bat")); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); new File(myOlderDir, "bin/idea.bat").delete(); PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); assertTrue(preparationResult.validationResults.isEmpty()); - assertAppliedAndRevertedCorrectly(preparationResult); + assertAppliedAndRevertedCorrectly(patch, preparationResult); } @Test public void testRevertingWithAbsentFileToDelete() throws Exception { - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); new File(myOlderDir, "bin/idea.bat").delete(); PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); - preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction()); - assertNothingHasChanged(preparationResult, new HashMap()); + preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch)); + assertNothingHasChanged(patch, preparationResult, new HashMap()); } @Test public void testApplyingWithoutCriticalFiles() throws Exception { - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); assertTrue(PatchFileCreator.apply(preparationResult, new HashMap(), TEST_UI)); @@ -171,24 +162,49 @@ public class PatchFileCreatorTest extends PatchTestCase { @Test public void testApplyingWithCriticalFiles() throws Exception { - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Arrays.asList("lib/annotations.jar"), - Collections.emptyList(), TEST_UI); + myPatchSpec.setCriticalFiles(Arrays.asList("lib/annotations.jar")); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); - PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + } - assertTrue(PatchFileCreator.apply(preparationResult, new HashMap(), TEST_UI)); - assertAppliedCorrectly(); + @Test + public void testApplyingWithModifiedCriticalFiles() throws Exception { + myPatchSpec.setStrict(true); + myPatchSpec.setCriticalFiles(Arrays.asList("lib/annotations.jar")); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + RandomAccessFile raf = new RandomAccessFile(new File(myOlderDir, "lib/annotations.jar"), "rw"); + raf.seek(20); + raf.write(42); + raf.close(); + + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + } + + @Test + public void testApplyingWithModifiedCriticalFilesAndDifferentRoot() throws Exception { + myPatchSpec.setStrict(true); + myPatchSpec.setRoot("lib/"); + myPatchSpec.setCriticalFiles(Arrays.asList("lib/annotations.jar")); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + RandomAccessFile raf = new RandomAccessFile(new File(myOlderDir, "lib/annotations.jar"), "rw"); + raf.seek(20); + raf.write(42); + raf.close(); + + File toDir = new File(myOlderDir, "lib/"); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, toDir, TEST_UI)); } @Test public void testApplyingWithCaseChangedNames() throws Exception { - FileUtil.rename(new File(myOlderDir, "Readme.txt"), - new File(myOlderDir, "README.txt")); + FileUtil.rename(new File(myOlderDir, "Readme.txt"), new File(myOlderDir, "README.txt")); - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); - assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); } @Test @@ -201,13 +217,11 @@ public class PatchFileCreatorTest extends PatchTestCase { new File(file, "subDir").mkdir(); new File(file, "subDir/subFile.txt").createNewFile(); - FileUtil.copy(new File(myOlderDir, "lib/boot.jar"), - new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar")); + FileUtil.copy(new File(myOlderDir, "lib/boot.jar"), new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar")); - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); - assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); } @Test @@ -219,15 +233,14 @@ public class PatchFileCreatorTest extends PatchTestCase { FileUtil.copy(new File(myOlderDir, "lib/boot_with_directory_becomes_file.jar"), new File(myOlderDir, "lib/boot.jar")); - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); - assertAppliedAndRevertedCorrectly(PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); + assertAppliedAndRevertedCorrectly(patch, PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI)); } @Test public void testConsideringOptions() throws Exception { - createPatch(); + Patch patch = createPatch(); PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); Map options = new HashMap(); @@ -235,47 +248,241 @@ public class PatchFileCreatorTest extends PatchTestCase { options.put(each.getPath(), ValidationResult.Option.IGNORE); } - assertNothingHasChanged(preparationResult, options); + assertNothingHasChanged(patch, preparationResult, options); } - private void createPatch() throws IOException, OperationCancelledException { - PatchFileCreator.create(myOlderDir, myNewerDir, myFile, - Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), TEST_UI); + @Test + public void testApplyWhenCommonFileChanges() throws Exception { + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + FileUtil.copy(new File(myOlderDir, "/lib/bootstrap.jar"), + new File(myOlderDir, "/lib/boot.jar")); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertTrue(preparationResult.validationResults.isEmpty()); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testApplyWhenCommonFileChangesStrict() throws Exception { + myPatchSpec.setStrict(true); + PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + FileUtil.copy(new File(myOlderDir, "/lib/bootstrap.jar"), new File(myOlderDir, "/lib/boot.jar")); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertEquals(1, preparationResult.validationResults.size()); + assertEquals( + new ValidationResult(ValidationResult.Kind.ERROR, "lib/boot.jar", ValidationResult.Action.VALIDATE, ValidationResult.MODIFIED_MESSAGE, + ValidationResult.Option.NONE), preparationResult.validationResults.get(0)); + } + + @Test + public void testApplyWhenNewFileExists() throws Exception { + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + FileUtil.writeToFile(new File(myOlderDir, "newfile.txt"), "hello"); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertTrue(preparationResult.validationResults.isEmpty()); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testApplyWhenNewFileExistsStrict() throws Exception { + myPatchSpec.setStrict(true); + myPatchSpec.setDeleteFiles(Collections.singletonList("lib/java_pid.*\\.hprof")); + + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + FileUtil.writeToFile(new File(myOlderDir, "newfile.txt"), "hello"); + FileUtil.writeToFile(new File(myOlderDir, "lib/java_pid1234.hprof"), "bye!"); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertEquals(1, preparationResult.validationResults.size()); + assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT, "newfile.txt", ValidationResult.Action.VALIDATE, "Unexpected file", + ValidationResult.Option.DELETE), preparationResult.validationResults.get(0)); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testApplyWhenNewDeletableFileExistsStrict() throws Exception { + myPatchSpec.setStrict(true); + myPatchSpec.setDeleteFiles(Collections.singletonList("lib/java_pid.*\\.hprof")); + + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + FileUtil.writeToFile(new File(myOlderDir, "lib/java_pid1234.hprof"), "bye!"); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertEquals(0, preparationResult.validationResults.size()); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testApplyWhenNewDirectoryExistsStrict() throws Exception { + myPatchSpec.setStrict(true); + new File(myOlderDir, "delete").mkdirs(); + FileUtil.writeToFile(new File(myOlderDir, "delete/deleteme.txt"), "bye!"); + + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + + new File(myOlderDir, "unexpected_newdir").mkdirs(); + FileUtil.writeToFile(new File(myOlderDir, "unexpected_newdir/unexpected.txt"), "bye!"); + + new File(myOlderDir, "newDir").mkdir(); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertEquals(3, preparationResult.validationResults.size()); + assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT, + "unexpected_newdir/unexpected.txt", + ValidationResult.Action.VALIDATE, + "Unexpected file", + ValidationResult.Option.DELETE), preparationResult.validationResults.get(0)); + assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT, + "unexpected_newdir/", + ValidationResult.Action.VALIDATE, + "Unexpected file", + ValidationResult.Option.DELETE), preparationResult.validationResults.get(1)); + assertEquals(new ValidationResult(ValidationResult.Kind.CONFLICT, + "newDir/", + ValidationResult.Action.CREATE, + ValidationResult.ALREADY_EXISTS_MESSAGE, + ValidationResult.Option.REPLACE), preparationResult.validationResults.get(2)); + new File(myOlderDir, "newDir").delete(); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testMoveFileByContent() throws IOException, OperationCancelledException { + myPatchSpec.setStrict(true); + FileUtil.writeToFile(new File(myOlderDir, "move/from/this/directory/move.me"), "oldcontent"); + FileUtil.writeToFile(new File(myOlderDir, "a/deleted/file/that/is/a/copy/move.me"), "newcontent"); + FileUtil.writeToFile(new File(myNewerDir, "move/to/this/directory/move.me"), "newcontent"); + + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + PatchAction action = getAction(patch, "move/to/this/directory/move.me"); + assertTrue(action instanceof UpdateAction); + UpdateAction update = (UpdateAction)action; + assertTrue(update.isMove()); + assertEquals("a/deleted/file/that/is/a/copy/move.me", update.getSourcePath()); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testMoveCriticalFileByContent() throws IOException, OperationCancelledException { + myPatchSpec.setStrict(true); + myPatchSpec.setCriticalFiles(Collections.singletonList("a/deleted/file/that/is/a/copy/move.me")); + + FileUtil.writeToFile(new File(myOlderDir, "move/from/this/directory/move.me"), "oldcontent"); + FileUtil.writeToFile(new File(myOlderDir, "a/deleted/file/that/is/a/copy/move.me"), "newcontent"); + FileUtil.writeToFile(new File(myNewerDir, "move/to/this/directory/move.me"), "newcontent"); + + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + PatchAction action = getAction(patch, "move/to/this/directory/move.me"); + assertTrue(action instanceof CreateAction); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testDontMoveFromDirectoryToFile() throws IOException, OperationCancelledException { + myPatchSpec.setStrict(true); + new File(myOlderDir, "from/move.me").mkdirs(); + FileUtil.writeToFile(new File(myNewerDir, "move/to/move.me"), "different"); + + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + // Creating a patch would have crashed if the directory had been chosen. + PatchAction action = getAction(patch, "move/to/move.me"); + assertTrue(action instanceof CreateAction); + action = getAction(patch, "from/move.me/"); + assertTrue(action instanceof DeleteAction); + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertEquals(0, preparationResult.validationResults.size()); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + @Test + public void testMoveFileByLocation() throws IOException, OperationCancelledException { + myPatchSpec.setStrict(true); + FileUtil.writeToFile(new File(myOlderDir, "move/from/this/directory/move.me"), "they"); + FileUtil.writeToFile(new File(myOlderDir, "not/from/this/one/move.me"), "are"); + FileUtil.writeToFile(new File(myNewerDir, "move/to/this/directory/move.me"), "different"); + + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); + PatchAction action = getAction(patch, "move/to/this/directory/move.me"); + assertTrue(action instanceof UpdateAction); + UpdateAction update = (UpdateAction)action; + assertTrue(!update.isMove()); + assertEquals("move/from/this/directory/move.me", update.getSourcePath()); + + PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI); + assertAppliedAndRevertedCorrectly(patch, preparationResult); + } + + protected PatchAction getAction(Patch patch, String path) { + for (PatchAction action : patch.getActions()) { + if (action.getPath().equals(path)) { + return action; + } + } + return null; + } + + protected Patch createPatch() throws IOException, OperationCancelledException { + Patch patch = PatchFileCreator.create(myPatchSpec, myFile, TEST_UI); assertTrue(myFile.exists()); + return patch; } - private void assertNothingHasChanged(PatchFileCreator.PreparationResult preparationResult, Map options) + private void assertNothingHasChanged(Patch patch, PatchFileCreator.PreparationResult preparationResult, Map options) throws Exception { - Map before = Digester.digestFiles(myOlderDir, Collections.emptyList(), TEST_UI); + Map before = patch.digestFiles(myOlderDir, Collections.emptyList(), false, TEST_UI); PatchFileCreator.apply(preparationResult, options, TEST_UI); - Map after = Digester.digestFiles(myOlderDir, Collections.emptyList(), TEST_UI); + Map after = patch.digestFiles(myOlderDir, Collections.emptyList(), false, TEST_UI); - DiffCalculator.Result diff = DiffCalculator.calculate(before, after); + DiffCalculator.Result diff = DiffCalculator.calculate(before, after, new LinkedList(), false); assertTrue(diff.filesToCreate.isEmpty()); assertTrue(diff.filesToDelete.isEmpty()); assertTrue(diff.filesToUpdate.isEmpty()); } - private void assertAppliedAndRevertedCorrectly(PatchFileCreator.PreparationResult preparationResult) + private void assertAppliedAndRevertedCorrectly(Patch patch, PatchFileCreator.PreparationResult preparationResult) throws IOException, OperationCancelledException { - Map original = Digester.digestFiles(myOlderDir, Collections.emptyList(), TEST_UI); - + Map original = patch.digestFiles(myOlderDir, Collections.emptyList(), false, TEST_UI); + Map target = patch.digestFiles(myNewerDir, Collections.emptyList(), false, TEST_UI); File backup = getTempFile("backup"); + HashMap options = new HashMap(); for (ValidationResult each : preparationResult.validationResults) { - assertTrue(each.toString(), each.kind != ValidationResult.Kind.ERROR); + if (patch.isStrict()) { + assertFalse(each.options.contains(ValidationResult.Option.NONE)); + assertTrue(each.options.size() > 0); + options.put(each.path, each.options.get(0)); + } else { + assertTrue(each.toString(), each.kind != ValidationResult.Kind.ERROR); + } } List appliedActions = - PatchFileCreator.apply(preparationResult, new HashMap(), backup, TEST_UI).appliedActions; - assertAppliedCorrectly(); + PatchFileCreator.apply(preparationResult, options, backup, TEST_UI).appliedActions; + Map patched = patch.digestFiles(myOlderDir, Collections.emptyList(), false, TEST_UI); - assertFalse(original.equals(Digester.digestFiles(myOlderDir, Collections.emptyList(), TEST_UI))); + if (patch.isStrict()) { + assertEquals(patched, target); + } else { + assertAppliedCorrectly(); + } + + assertNotEquals(original, patched); PatchFileCreator.revert(preparationResult, appliedActions, backup, TEST_UI); - - assertEquals(original, Digester.digestFiles(myOlderDir, Collections.emptyList(), TEST_UI)); + Map reverted = patch.digestFiles(myOlderDir, Collections.emptyList(), false, TEST_UI); + assertEquals(original, reverted); } protected void assertAppliedCorrectly() throws IOException { @@ -333,8 +540,11 @@ public class PatchFileCreatorTest extends PatchTestCase { } private static class MyFailOnApplyPatchAction extends PatchAction { - public MyFailOnApplyPatchAction() { - super("_dummy_file_", -1); + // Only used on patch creation + protected transient File myOlderDir; + + public MyFailOnApplyPatchAction(Patch patch) { + super(patch, "_dummy_file_", Digester.INVALID); } @Override @@ -348,12 +558,12 @@ public class PatchFileCreatorTest extends PatchTestCase { } @Override - protected ValidationResult doValidate(File toFile) throws IOException { + protected ValidationResult validate(File toDir) throws IOException { throw new UnsupportedOperationException(); } @Override - protected void doApply(ZipFile patchFile, File toFile) throws IOException { + protected void doApply(ZipFile patchFile, File backupDir, File toFile) throws IOException { throw new IOException("dummy exception"); } diff --git a/updater/testSrc/com/intellij/updater/PatchTest.java b/updater/testSrc/com/intellij/updater/PatchTest.java index b64ad9f51406..751ce733ef84 100644 --- a/updater/testSrc/com/intellij/updater/PatchTest.java +++ b/updater/testSrc/com/intellij/updater/PatchTest.java @@ -20,26 +20,34 @@ public class PatchTest extends PatchTestCase { @Before public void setUp() throws Exception { super.setUp(); - myPatch = new Patch(myOlderDir, myNewerDir, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), TEST_UI); + PatchSpec spec = new PatchSpec() + .setOldFolder(myOlderDir.getAbsolutePath()) + .setNewFolder(myNewerDir.getAbsolutePath()); + myPatch = new Patch(spec, TEST_UI); + } + + @Test + public void testDigestFiles() throws Exception { + Map checkSums = myPatch.digestFiles(getDataDir(), Collections.emptyList(), false, TEST_UI); + assertEquals(9, checkSums.size()); } @Test public void testBasics() throws Exception { List expectedActions = Arrays.asList( - new CreateAction("newDir/newFile.txt"), - new UpdateAction("Readme.txt", CHECKSUMS.README_TXT), - new UpdateZipAction("lib/annotations.jar", + new CreateAction(myPatch, "newDir/newFile.txt"), + new UpdateAction(myPatch, "Readme.txt", CHECKSUMS.README_TXT), + new UpdateZipAction(myPatch, "lib/annotations.jar", Arrays.asList("org/jetbrains/annotations/NewClass.class"), Arrays.asList("org/jetbrains/annotations/Nullable.class"), Arrays.asList("org/jetbrains/annotations/TestOnly.class"), CHECKSUMS.ANNOTATIONS_JAR), - new UpdateZipAction("lib/bootstrap.jar", + new UpdateZipAction(myPatch, "lib/bootstrap.jar", Collections.emptyList(), Collections.emptyList(), Arrays.asList("com/intellij/ide/ClassloaderUtil.class"), CHECKSUMS.BOOTSTRAP_JAR), - new DeleteAction("bin/idea.bat", CHECKSUMS.IDEA_BAT)); + new DeleteAction(myPatch, "bin/idea.bat", CHECKSUMS.IDEA_BAT)); List actualActions = new ArrayList(myPatch.getActions()); Collections.sort(expectedActions, COMPARATOR); Collections.sort(actualActions, COMPARATOR); @@ -48,21 +56,21 @@ public class PatchTest extends PatchTestCase { @Test public void testCreatingWithIgnoredFiles() throws Exception { - myPatch = new Patch(myOlderDir, - myNewerDir, - Arrays.asList("Readme.txt", "bin/idea.bat"), - Collections.emptyList(), - Collections.emptyList(), + PatchSpec spec = new PatchSpec() + .setOldFolder(myOlderDir.getAbsolutePath()) + .setNewFolder(myNewerDir.getAbsolutePath()) + .setIgnoredFiles(Arrays.asList("Readme.txt", "bin/idea.bat")); + myPatch = new Patch(spec, TEST_UI); List expectedActions = Arrays.asList( - new CreateAction("newDir/newFile.txt"), - new UpdateZipAction("lib/annotations.jar", + new CreateAction(myPatch, "newDir/newFile.txt"), + new UpdateZipAction(myPatch, "lib/annotations.jar", Arrays.asList("org/jetbrains/annotations/NewClass.class"), Arrays.asList("org/jetbrains/annotations/Nullable.class"), Arrays.asList("org/jetbrains/annotations/TestOnly.class"), CHECKSUMS.ANNOTATIONS_JAR), - new UpdateZipAction("lib/bootstrap.jar", + new UpdateZipAction(myPatch, "lib/bootstrap.jar", Collections.emptyList(), Collections.emptyList(), Arrays.asList("com/intellij/ide/ClassloaderUtil.class"), @@ -125,8 +133,11 @@ public class PatchTest extends PatchTestCase { ValidationResult.Option.IGNORE))), new HashSet(myPatch.validate(myOlderDir, TEST_UI))); - myPatch = new Patch(myOlderDir, myNewerDir, Collections.emptyList(), Collections.emptyList(), - Arrays.asList("lib/annotations.jar"), TEST_UI); + PatchSpec spec = new PatchSpec() + .setOldFolder(myOlderDir.getAbsolutePath()) + .setNewFolder(myNewerDir.getAbsolutePath()) + .setOptionalFiles(Arrays.asList("lib/annotations.jar")); + myPatch = new Patch(spec, TEST_UI); FileUtil.delete(new File(myOlderDir, "lib/annotations.jar")); assertEquals(Collections.emptyList(), myPatch.validate(myOlderDir, TEST_UI)); diff --git a/updater/testSrc/com/intellij/updater/RunnerTest.java b/updater/testSrc/com/intellij/updater/RunnerTest.java index 22464a0fa053..9d11618de453 100644 --- a/updater/testSrc/com/intellij/updater/RunnerTest.java +++ b/updater/testSrc/com/intellij/updater/RunnerTest.java @@ -14,12 +14,12 @@ public class RunnerTest extends UpdaterTestCase { Runner.initLogger(); assertEquals(Arrays.asList("xxx", "yyy", "zzz/zzz", "aaa"), - Runner.extractFiles(args, "ignored")); + Runner.extractArguments(args, "ignored")); assertEquals(Arrays.asList("ccc"), - Runner.extractFiles(args, "critical")); + Runner.extractArguments(args, "critical")); assertEquals(Collections.emptyList(), - Runner.extractFiles(args, "unknown")); + Runner.extractArguments(args, "unknown")); } } diff --git a/updater/testSrc/com/intellij/updater/UpdaterTestCase.java b/updater/testSrc/com/intellij/updater/UpdaterTestCase.java index dea5fbdd7c6d..075e17b6a181 100644 --- a/updater/testSrc/com/intellij/updater/UpdaterTestCase.java +++ b/updater/testSrc/com/intellij/updater/UpdaterTestCase.java @@ -18,6 +18,15 @@ public abstract class UpdaterTestCase { @Override public void setStatus(String status) { } + + @Override + public void setDescription(String oldBuildDesc, String newBuildDesc) { + } + + @Override + public boolean showWarning(String message) { + return false; + } }; protected CheckSums CHECKSUMS; @@ -54,7 +63,16 @@ public abstract class UpdaterTestCase { public final long IDEA_BAT; public final long ANNOTATIONS_JAR; public final long BOOTSTRAP_JAR; + public final long BOOTSTRAP_JAR_BINARY; public final long FOCUSKILLER_DLL; + public final long ANNOTATIONS_JAR_NORM; + public final long ANNOTATIONS_CHANGED_JAR_NORM; + public final long BOOT_JAR_NORM; + public final long BOOT2_JAR_NORM; + public final long BOOT2_CHANGED_WITH_UNCHANGED_CONTENT_JAR_NORM; + public final long BOOT_WITH_DIRECTORY_BECOMES_FILE_JAR_NORM; + public final long BOOTSTRAP_JAR_NORM; + public final long BOOTSTRAP_DELETED_JAR_NORM; public CheckSums(boolean windowsLineEnds) { if (windowsLineEnds) { @@ -68,6 +86,16 @@ public abstract class UpdaterTestCase { ANNOTATIONS_JAR = 2119442657L; BOOTSTRAP_JAR = 2082851308L; FOCUSKILLER_DLL = 1991212227L; + BOOTSTRAP_JAR_BINARY = 2745721972L; + + ANNOTATIONS_JAR_NORM = 2119442657L; + ANNOTATIONS_CHANGED_JAR_NORM = 4088078858L; + BOOT_JAR_NORM = 3018038682L; + BOOT2_JAR_NORM = 2406818996L; + BOOT2_CHANGED_WITH_UNCHANGED_CONTENT_JAR_NORM = 2406818996L; + BOOT_WITH_DIRECTORY_BECOMES_FILE_JAR_NORM = 1972168924; + BOOTSTRAP_JAR_NORM = 2082851308; + BOOTSTRAP_DELETED_JAR_NORM = 544883981L; } } } \ No newline at end of file