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
This commit is contained in:
Vladimir.Orlov
2015-01-14 11:29:07 +03:00
parent 526fb09533
commit a7ca0f548a
29 changed files with 1696 additions and 612 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) {
return Collections.emptyMap();
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, Long> oldChecksums, Map<String, Long> newChecksums) {
public static Result calculate(Map<String, Long> oldChecksums, Map<String, Long> newChecksums, List<String> 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<String, Long> toUpdate = collect(oldChecksums, newChecksums, critical, false);
Map<String, Long> toCreate = withAllRemoved(newChecksums, oldChecksums);
// Some creates become updates if found in different directories.
result.filesToCreate = new LinkedHashMap<String, Long>();
result.filesToUpdate = new LinkedHashMap<String, Update>();
for (Map.Entry<String, Long> update : toUpdate.entrySet()) {
result.filesToUpdate.put(update.getKey(), new Update(update.getKey(), update.getValue(), false));
}
if (move) {
Map<Long, String> byContent = inverse(result.filesToDelete);
Map<String, List<String>> byName = groupFilesByName(result.filesToDelete);
// Find first by content
for (Map.Entry<String, Long> 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<String> 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<String> 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<String, List<String>> groupFilesByName(Map<String, Long> toDelete) {
Map<String, List<String>> result = new HashMap<String, List<String>>();
for (String path : toDelete.keySet()) {
if (!path.endsWith("/")) {
String name = new File(path).getName();
List<String> paths = result.get(name);
if (paths == null) {
paths = new LinkedList<String>();
result.put(name, paths);
}
paths.add(path);
}
}
return result;
}
public static Map<Long,String> inverse(Map<String, Long> map) {
Map<Long, String> inv = new LinkedHashMap<Long, String>();
for (Map.Entry<String, Long> entry : map.entrySet()) {
inv.put(entry.getValue(), entry.getKey());
}
return inv;
}
private static Map<String, Long> withAllRemoved(Map<String, Long> from, Map<String, Long> toRemove) {
Map<String, Long> result = new HashMap<String, Long>(from);
Map<String, Long> result = new LinkedHashMap<String, Long>(from);
for (String each : toRemove.keySet()) {
result.remove(each);
}
return result;
}
private static Map<String, Long> collect(Map<String, Long> older, Map<String, Long> newer, boolean equal) {
Map<String, Long> result = new HashMap<String, Long>();
private static Map<String, Long> collect(Map<String, Long> older, Map<String, Long> newer, List<String> critical, boolean equal) {
Map<String, Long> result = new LinkedHashMap<String, Long>();
for (Map.Entry<String, Long> 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<String, Long> filesToDelete;
public Set<String> filesToCreate;
public Map<String, Long> filesToUpdate;
public Map<String, Long> filesToCreate;
public Map<String, Update> filesToUpdate;
public Map<String, Long> commonFiles;
}
}

View File

@@ -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<String, Long> digestFiles(File dir, List<String> ignoredFiles, UpdaterUI ui)
throws IOException, OperationCancelledException {
Map<String, Long> result = new HashMap<String, Long>();
// CRC32 will only use the lower 32bits of long, never returning negative values.
public static long INVALID = -1;
public static long DIRECTORY = -2;
LinkedHashSet<String> 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<ZipEntry> sorted = new ArrayList<ZipEntry>();
Enumeration<? extends ZipEntry> 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<ZipEntry> sorted = new ArrayList<ZipEntry>();
Collections.sort(sorted, new Comparator<ZipEntry>() {
public int compare(ZipEntry o1, ZipEntry o2) {
return o1.getName().compareTo(o2.getName());
Enumeration<? extends ZipEntry> 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<ZipEntry>() {
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 {

View File

@@ -6,64 +6,75 @@ import java.util.zip.ZipFile;
public class Patch {
private List<PatchAction> myActions = new ArrayList<PatchAction>();
private boolean myIsBinary;
private boolean myIsStrict;
private boolean myIsNormalized;
private String myOldBuild;
private String myNewBuild;
private String myRoot;
private Map<String, String> myWarnings;
private List<String> 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<String> ignoredFiles,
List<String> criticalFiles,
List<String> 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<String> ignoredFiles,
List<String> criticalFiles,
List<String> 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<PatchAction> tempActions = new ArrayList<PatchAction>();
// 'delete' actions before 'create' actions to prevent newly created files to be deleted if the names differ only on case.
for (Map.Entry<String, Long> 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<String, Long> each : diff.filesToUpdate.entrySet()) {
if (!Runner.ZIP_AS_BINARY && Utils.isZipFile(each.getKey())) {
tempActions.add(new UpdateZipAction(each.getKey(), each.getValue()));
for (Map.Entry<String, DiffCalculator.Update> 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<String, Long> 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<String> list) throws IOException {
dataOut.writeInt(list.size());
for (String string : list) {
dataOut.writeUTF(string);
}
}
private static void writeMap(DataOutputStream dataOut, Map<String, String> map) throws IOException {
dataOut.writeInt(map.size());
for (Map.Entry<String, String> entry : map.entrySet()) {
dataOut.writeUTF(entry.getKey());
dataOut.writeUTF(entry.getValue());
}
}
private void writeActions(DataOutputStream dataOut, List<PatchAction> 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<PatchAction> newActions = new ArrayList<PatchAction>();
@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<String> readList(DataInputStream in) throws IOException {
int size = in.readInt();
List<String> list = new ArrayList<String>(size);
for (int i = 0; i < size; i++) {
list.add(in.readUTF());
}
return list;
}
private static Map<String, String> readMap(DataInputStream in) throws IOException {
int size = in.readInt();
Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < size; i++) {
String key = in.readUTF();
map.put(key, in.readUTF());
}
return map;
}
private List<PatchAction> readActions(DataInputStream in) throws IOException {
List<PatchAction> actions = new ArrayList<PatchAction>();
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<ValidationResult> validate(final File toDir, UpdaterUI ui) throws IOException, OperationCancelledException {
final LinkedHashSet<String> 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<ValidationResult> validate(final File rootDir, UpdaterUI ui) throws IOException, OperationCancelledException {
LinkedHashSet<String> 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<ValidationResult> result = new ArrayList<ValidationResult>();
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<String, ValidationResult.Option> options,
UpdaterUI ui) throws IOException, OperationCancelledException {
final File toDir = toBaseDir(rootDir);
List<PatchAction> actionsToProcess = new ArrayList<PatchAction>();
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<PatchAction> actions, final File backupDir, final File toDir, UpdaterUI ui)
public void revert(List<PatchAction> 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<String, Long> digestFiles(File dir, List<String> ignoredFiles, boolean normalize, UpdaterUI ui)
throws IOException, OperationCancelledException {
Map<String, Long> result = new LinkedHashMap<String, Long>();
LinkedHashSet<String> 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;
}

View File

@@ -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);
}

View File

@@ -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<String> ignoredFiles,
List<String> criticalFiles,
List<String> 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<PatchAction> 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<ValidationResult> validationResults = patch.validate(toDir, ui);
return new PreparationResult(patch, patchFile, toDir, validationResults);
}

View File

@@ -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<String> myIgnoredFiles = Collections.emptyList();
private List<String> myCriticalFiles = Collections.emptyList();
private List<String> myOptionalFiles = Collections.emptyList();
private boolean myIsNormalized;
private Map<String, String> myWarnings = Collections.emptyMap();
private List<String> 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<String> getIgnoredFiles() {
return myIgnoredFiles;
}
public PatchSpec setIgnoredFiles(List<String> ignoredFiles) {
myIgnoredFiles = ignoredFiles;
return this;
}
public List<String> getCriticalFiles() {
return myCriticalFiles;
}
public PatchSpec setCriticalFiles(List<String> criticalFiles) {
myCriticalFiles = criticalFiles;
return this;
}
public List<String> getOptionalFiles() {
return myOptionalFiles;
}
public PatchSpec setOptionalFiles(List<String> 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<String, String> warnings) {
myWarnings = warnings;
return this;
}
public Map<String, String> getWarnings() {
return myWarnings;
}
public PatchSpec setDeleteFiles(List<String> deleteFiles) {
myDeleteFiles = deleteFiles;
return this;
}
public List<String> getDeleteFiles() {
return myDeleteFiles;
}
public PatchSpec setRoot(String root) {
myRoot = root;
return this;
}
public String getRoot() {
return myRoot;
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> ignoredFiles = extractFiles(args, "ignored");
List<String> criticalFiles = extractFiles(args, "critical");
List<String> 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<String> ignoredFiles = extractArguments(args, "ignored");
List<String> criticalFiles = extractArguments(args, "critical");
List<String> optionalFiles = extractArguments(args, "optional");
List<String> deleteFiles = extractArguments(args, "delete");
Map<String, String> 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<String, String> buildWarningMap(List<String> warnings) {
Map<String, String> map = new HashMap<String, String>();
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<String> 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<String> extractArguments(String[] args, String paramName) {
List<String> result = new ArrayList<String>();
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 <old_version_description> <new_version_description> <old_version_folder> <new_version_folder>" +
" <patch_file_name> [ignored=file1;file2;...] [critical=file1;file2;...] [optional=file1;file2;...]\n" +
"install <destination_folder>\n");
System.err.println(
"Usage:\n" +
" Runner create <old_version> <new_version> <old_folder> <new_folder> <patch_file> [<file_set>=file1;file2;...] [<flags>]\n" +
" Runner install <folder>\n" +
"\n" +
"Where:\n" +
" <old_version>: A description of the version to generate the patch from.\n" +
" <new_version>: A description of the version to generate the patch to.\n" +
" <old_folder>: The folder where to find the old version.\n" +
" <new_folder>: The folder where to find the new version.\n" +
" <patch_file>: The .jar patch file to create which contains the patch and the patcher.\n" +
" <file_set>: 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" +
" <flags>: 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=<dir>: 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 <old_folder> 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<String> ignoredFiles,
List<String> criticalFiles,
List<String> 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);

View File

@@ -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("<html>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("<html>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 = "<html>There are some conflicts found in the installation.<br><br>" +
"Please select desired solutions from the " + MyTableModel.COLUMNS[MyTableModel.OPTIONS_COLUMN_INDEX] +
" column and press " + PROCEED_BUTTON_TITLE + ".<br>" +
"If you do not want to proceed with the update, please press " + CANCEL_BUTTON_TITLE + ".</html>";
String message = "<html>Some conflicts were found in the installation area.<br><br>";
if (proceed) {
message += "Please select desired solutions from the " + MyTableModel.COLUMNS[MyTableModel.OPTIONS_COLUMN_INDEX] +
" column and press " + PROCEED_BUTTON_TITLE + ".<br>" +
"If you do not want to proceed with the update, please press " + CANCEL_BUTTON_TITLE + ".</html>";
} else {
message += "Some of the conflicts below do not have a solution, so the patch cannot be applied.<br>" +
"Press " + CANCEL_BUTTON_TITLE + " to exit.</html>";
}
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();

View File

@@ -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);
}
}

View File

@@ -12,24 +12,24 @@ public class UpdateZipAction extends BaseUpdateAction {
Set<String> myFilesToUpdate;
Set<String> 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<String> filesToCreate,
Collection<String> filesToUpdate,
Collection<String> filesToDelete,
long checksum) {
super(path, checksum);
super(patch, path, path, checksum, false);
myFilesToCreate = new HashSet<String>(filesToCreate);
myFilesToUpdate = new HashSet<String>(filesToUpdate);
myFilesToDelete = new HashSet<String>(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<String>(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<String>(), 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;

View File

@@ -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<String, ValidationResult.Option> askUser(List<ValidationResult> validationResults) throws OperationCancelledException;
}

View File

@@ -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<String> collectRelativePaths(File dir) {
public static LinkedHashSet<String> collectRelativePaths(File dir, boolean includeDirectories) {
LinkedHashSet<String> result = new LinkedHashSet<String>();
collectRelativePaths(dir, result, null);
collectRelativePaths(dir, result, null, includeDirectories);
return result;
}
private static void collectRelativePaths(File dir, LinkedHashSet<String> result, String parentPath) {
private static void collectRelativePaths(File dir, LinkedHashSet<String> 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<? extends ZipEntry> 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<ZipEntry>() {
@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();
}
}
}

View File

@@ -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 {
}
}

View File

@@ -9,7 +9,7 @@ public class ValidationResult implements Comparable<ValidationResult> {
}
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<ValidationResult> {
}
public enum Option {
IGNORE, KEEP, REPLACE, DELETE, KILL_PROCESS
NONE, IGNORE, KEEP, REPLACE, DELETE, KILL_PROCESS
}
public static final String ABSENT_MESSAGE = "Absent";

View File

@@ -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

View File

@@ -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<String, Long> checkSums = Digester.digestFiles(getDataDir(), Collections.<String>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));
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<String, ValidationResult.Option>());
}
@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<String, ValidationResult.Option>());
preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch));
assertNothingHasChanged(patch, preparationResult, new HashMap<String, ValidationResult.Option>());
}
@Test
public void testRevertedWhenFileToDeleteIsProcessLocked() throws Exception {
if (!UtilsTest.mIsWindows) return;
PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>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<String, Long> original = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
Map<String, Long> original = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
File backup = getTempFile("backup");
PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), backup, TEST_UI);
assertEquals(original, Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI));
assertEquals(original, patch.digestFiles(myOlderDir, Collections.<String>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.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>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.<String>emptyList(), Collections.<String>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.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>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<String, ValidationResult.Option>());
preparationResult.patch.getActions().add(new MyFailOnApplyPatchAction(patch));
assertNothingHasChanged(patch, preparationResult, new HashMap<String, ValidationResult.Option>());
}
@Test
public void testApplyingWithoutCriticalFiles() throws Exception {
PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>emptyList(), TEST_UI);
PatchFileCreator.create(myPatchSpec, myFile, TEST_UI);
PatchFileCreator.PreparationResult preparationResult = PatchFileCreator.prepareAndValidate(myFile, myOlderDir, TEST_UI);
assertTrue(PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), TEST_UI));
@@ -171,24 +162,49 @@ public class PatchFileCreatorTest extends PatchTestCase {
@Test
public void testApplyingWithCriticalFiles() throws Exception {
PatchFileCreator.create(myOlderDir, myNewerDir, myFile, Collections.<String>emptyList(), Arrays.asList("lib/annotations.jar"),
Collections.<String>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<String, ValidationResult.Option>(), 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.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>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.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>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.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>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<String, ValidationResult.Option> options = new HashMap<String, ValidationResult.Option>();
@@ -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.<String>emptyList(), Collections.<String>emptyList(), Collections.<String>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<String, ValidationResult.Option> options)
private void assertNothingHasChanged(Patch patch, PatchFileCreator.PreparationResult preparationResult, Map<String, ValidationResult.Option> options)
throws Exception {
Map<String, Long> before = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
Map<String, Long> before = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
PatchFileCreator.apply(preparationResult, options, TEST_UI);
Map<String, Long> after = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
Map<String, Long> after = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
DiffCalculator.Result diff = DiffCalculator.calculate(before, after);
DiffCalculator.Result diff = DiffCalculator.calculate(before, after, new LinkedList<String>(), 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<String, Long> original = Digester.digestFiles(myOlderDir, Collections.<String>emptyList(), TEST_UI);
Map<String, Long> original = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
Map<String, Long> target = patch.digestFiles(myNewerDir, Collections.<String>emptyList(), false, TEST_UI);
File backup = getTempFile("backup");
HashMap<String, ValidationResult.Option> options = new HashMap<String, ValidationResult.Option>();
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<PatchAction> appliedActions =
PatchFileCreator.apply(preparationResult, new HashMap<String, ValidationResult.Option>(), backup, TEST_UI).appliedActions;
assertAppliedCorrectly();
PatchFileCreator.apply(preparationResult, options, backup, TEST_UI).appliedActions;
Map<String, Long> patched = patch.digestFiles(myOlderDir, Collections.<String>emptyList(), false, TEST_UI);
assertFalse(original.equals(Digester.digestFiles(myOlderDir, Collections.<String>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.<String>emptyList(), TEST_UI));
Map<String, Long> reverted = patch.digestFiles(myOlderDir, Collections.<String>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");
}

View File

@@ -20,26 +20,34 @@ public class PatchTest extends PatchTestCase {
@Before
public void setUp() throws Exception {
super.setUp();
myPatch = new Patch(myOlderDir, myNewerDir, Collections.<String>emptyList(), Collections.<String>emptyList(),
Collections.<String>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<String, Long> checkSums = myPatch.digestFiles(getDataDir(), Collections.<String>emptyList(), false, TEST_UI);
assertEquals(9, checkSums.size());
}
@Test
public void testBasics() throws Exception {
List<PatchAction> 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.<String>emptyList(),
Collections.<String>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<PatchAction> actualActions = new ArrayList<PatchAction>(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.<String>emptyList(),
Collections.<String>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<PatchAction> 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.<String>emptyList(),
Collections.<String>emptyList(),
Arrays.asList("com/intellij/ide/ClassloaderUtil.class"),
@@ -125,8 +133,11 @@ public class PatchTest extends PatchTestCase {
ValidationResult.Option.IGNORE))),
new HashSet<ValidationResult>(myPatch.validate(myOlderDir, TEST_UI)));
myPatch = new Patch(myOlderDir, myNewerDir, Collections.<String>emptyList(), Collections.<String>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.<ValidationResult>emptyList(),
myPatch.validate(myOlderDir, TEST_UI));

View File

@@ -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.<String>emptyList(),
Runner.extractFiles(args, "unknown"));
Runner.extractArguments(args, "unknown"));
}
}

View File

@@ -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;
}
}
}