/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* 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.*;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public abstract class PatchAction {
public enum FileType {
REGULAR_FILE, EXECUTABLE_FILE, SYMLINK
}
private static final byte CRITICAL = 0x1;
private static final byte OPTIONAL = 0x2;
protected final transient Patch myPatch;
private final String myPath;
private final long myChecksum;
private byte myFlags;
public PatchAction(Patch patch, String path, long checksum) {
this(patch, path, checksum, (byte)0);
}
public PatchAction(Patch patch, DataInputStream in) throws IOException {
this(patch, in.readUTF(), in.readLong(), in.readByte());
}
private PatchAction(Patch patch, String path, long checksum, byte flags) {
myPatch = patch;
myPath = path;
myChecksum = checksum;
myFlags = flags;
}
public void write(DataOutputStream out) throws IOException {
out.writeUTF(myPath);
out.writeLong(myChecksum);
out.writeByte(myFlags);
}
public String getPath() {
return myPath;
}
protected File getFile(File baseDir) {
return new File(baseDir, myPath);
}
public long getChecksum() {
return myChecksum;
}
public boolean isCritical() {
return (myFlags & CRITICAL) != 0;
}
public void setCritical(boolean critical) {
if (critical) myFlags |= CRITICAL; else myFlags &= ~CRITICAL;
}
public boolean isOptional() {
return (myFlags & OPTIONAL) != 0;
}
public void setOptional(boolean optional) {
if (optional) myFlags |= OPTIONAL; else myFlags &= ~OPTIONAL;
}
protected static FileType getFileType(File file) throws IOException {
if (Utils.isLink(file)) return FileType.SYMLINK;
if (Utils.isExecutable(file)) return FileType.EXECUTABLE_FILE;
return FileType.REGULAR_FILE;
}
protected static void writeFileType(OutputStream out, FileType type) throws IOException {
out.write(type.ordinal());
}
protected static FileType readFileType(InputStream in) throws IOException {
int value = in.read();
FileType[] types = FileType.values();
if (value < 0 || value >= types.length) throw new IOException("Stream format error");
return types[value];
}
public boolean calculate(File olderDir, File newerDir) throws IOException {
return doCalculate(getFile(olderDir), getFile(newerDir));
}
protected boolean doCalculate(File olderFile, File newerFile) throws IOException {
return true;
}
public void buildPatchFile(File olderDir, File newerDir, ZipOutputStream patchOutput) throws IOException {
doBuildPatchFile(getFile(olderDir), getFile(newerDir), patchOutput);
}
protected abstract void doBuildPatchFile(File olderFile, File newerFile, ZipOutputStream patchOutput) throws IOException;
public boolean shouldApply(File toDir, Map<String, ValidationResult.Option> options) {
File file = getFile(toDir);
ValidationResult.Option option = options.get(myPath);
if (option == ValidationResult.Option.KEEP || option == ValidationResult.Option.IGNORE) return false;
if (option == ValidationResult.Option.KILL_PROCESS) {
NativeFileManager.getProcessesUsing(file).forEach(p -> p.terminate());
}
return doShouldApply(toDir);
}
protected boolean doShouldApply(File toDir) {
return true;
}
protected abstract ValidationResult validate(File toDir) throws IOException;
protected ValidationResult doValidateAccess(File toFile, ValidationResult.Action action, boolean checkWriteable) {
if (!toFile.exists() || toFile.isDirectory()) return null;
ValidationResult result = validateProcessLock(toFile, action);
if (result != null) return result;
if (!checkWriteable) return null;
if (toFile.canRead() && toFile.canWrite() && isWritable(toFile)) return null;
ValidationResult.Option[] options = {myPatch.isStrict() ? ValidationResult.Option.NONE : ValidationResult.Option.IGNORE};
return new ValidationResult(ValidationResult.Kind.ERROR, myPath, action, ValidationResult.ACCESS_DENIED_MESSAGE, options);
}
private static boolean isWritable(File toFile) {
try (FileOutputStream s = new FileOutputStream(toFile, true); FileChannel ch = s.getChannel(); FileLock lock = ch.tryLock()) {
return lock != null;
}
catch (OverlappingFileLockException | IOException e) {
Runner.printStackTrace(e);
return false;
}
}
private ValidationResult validateProcessLock(File toFile, ValidationResult.Action action) {
List<NativeFileManager.Process> processes = NativeFileManager.getProcessesUsing(toFile);
if (processes.size() == 0) return null;
String message = "Locked by: " + processes.stream().map(p -> p.name).collect(Collectors.joining(", "));
return new ValidationResult(ValidationResult.Kind.ERROR, myPath, action, message, ValidationResult.Option.KILL_PROCESS);
}
protected ValidationResult doValidateNotChanged(File toFile, ValidationResult.Kind kind, ValidationResult.Action action) 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, options);
}
}
else if (!isOptional()) {
ValidationResult.Option[] options = {myPatch.isStrict() ? ValidationResult.Option.NONE : ValidationResult.Option.IGNORE};
return new ValidationResult(kind, myPath, action, ValidationResult.ABSENT_MESSAGE, options);
}
return null;
}
protected boolean isModified(File toFile) throws IOException {
return myChecksum == Digester.INVALID || myChecksum != myPatch.digestFile(toFile, myPatch.isNormalized());
}
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));
}
protected abstract void doBackup(File toFile, File backupFile) throws IOException;
public void revert(File toDir, File backupDir) throws IOException {
doRevert(getFile(toDir), getFile(backupDir));
}
protected abstract void doRevert(File toFile, File backupFile) throws IOException;
@Override
public String toString() {
return getClass().getSimpleName() + "(" + myPath + ", " + myChecksum + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PatchAction that = (PatchAction)o;
if (myChecksum != that.myChecksum) return false;
if (!Objects.equals(myPath, that.myPath)) return false;
return true;
}
@Override
public int hashCode() {
int result = Objects.hashCode(myPath);
result = 31 * result + (int)(myChecksum ^ (myChecksum >>> 32));
return result;
}
}