/* * This file is part of Fim - File Integrity Manager * * Copyright (C) 2017 Etienne Vrignaud * * Fim is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fim is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Fim. If not, see <http://www.gnu.org/licenses/>. */ package org.fim.model; import com.google.common.base.Charsets; import com.google.common.base.MoreObjects; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.rits.cloning.Cloner; import org.fim.util.Ascii85Util; import org.fim.util.FileUtil; import org.fim.util.JsonIO; import org.fim.util.Logger; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import static org.fim.model.HashMode.hashAll; import static org.fim.util.Ascii85Util.UTF8; public class State implements Hashable { public static final String CURRENT_MODEL_VERSION = "5"; private static final Cloner CLONER = new Cloner(); private static Comparator<FileState> fileNameComparator = new FileState.FileNameComparator(); private static JsonIO jsonIO = new JsonIO(); private String stateHash; // Ensure the integrity of the complete State content private String modelVersion; private long timestamp; private String comment; private int fileCount; private long filesContentLength; private HashMode hashMode; private CommitDetails commitDetails; private ModificationCounts modificationCounts; // Not taken in account in equals(), hashCode(), hashObject() private Set<String> ignoredFiles; private List<FileState> fileStates; public State() { modelVersion = CURRENT_MODEL_VERSION; timestamp = System.currentTimeMillis(); comment = ""; fileCount = 0; filesContentLength = 0; hashMode = hashAll; modificationCounts = new ModificationCounts(); ignoredFiles = new HashSet<>(); fileStates = new ArrayList<>(); commitDetails = new CommitDetails(hashMode, null); } public static State loadFromGZipFile(Path stateFile, boolean loadFullState) throws IOException, CorruptedStateException { try (Reader reader = new InputStreamReader(new GZIPInputStream(new FileInputStream(stateFile.toFile())), UTF8)) { State state = jsonIO.getObjectMapper().readValue(reader, State.class); System.gc(); // Force to cleanup unused memory if (state == null) { throw new CorruptedStateException(); } if (loadFullState) { if (!CURRENT_MODEL_VERSION.equals(state.getModelVersion())) { Logger.warning(String.format("State %s use a different model version. Some features will not work completely.", stateFile.getFileName().toString())); } else { checkIntegrity(state); } } return state; } } private static void checkIntegrity(State state) throws CorruptedStateException { String hash = state.hashState(); if (!state.stateHash.equals(hash)) { throw new CorruptedStateException(); } } public void saveToGZipFile(Path stateFile) throws IOException { Collections.sort(fileStates, fileNameComparator); updateFileCount(); updateFilesContentLength(); stateHash = hashState(); try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(new FileOutputStream(stateFile.toFile())), UTF8)) { jsonIO.getObjectWriter().writeValue(writer, this); } System.gc(); // Force to cleanup unused memory } public State filterDirectory(Path repositoryRootDir, Path currentDirectory, boolean keepFilesInside) { State filteredState = clone(); filteredState.getFileStates().clear(); String rootDir = FileUtil.getNormalizedFileName(repositoryRootDir); String curDir = FileUtil.getNormalizedFileName(currentDirectory); String subDirectory = FileUtil.getRelativeFileName(rootDir, curDir) + '/'; fileStates.stream() .filter(fileState -> fileState.getFileName().startsWith(subDirectory) == keepFilesInside) .forEach(fileState -> filteredState.getFileStates().add(fileState)); return filteredState; } public void updateFileCount() { fileCount = fileStates.size(); } public void updateFilesContentLength() { filesContentLength = 0; for (FileState fileState : fileStates) { filesContentLength += fileState.getFileLength(); } } public String getModelVersion() { return modelVersion; } public long getTimestamp() { return timestamp; } protected void setTimestamp(long timestamp) { this.timestamp = timestamp; } public String getComment() { return comment; } public void setComment(String comment) { // Intern Strings to decrease memory usage this.comment = comment.intern(); } public int getFileCount() { updateFileCount(); return fileCount; } public long getFilesContentLength() { updateFilesContentLength(); return filesContentLength; } public HashMode getHashMode() { return hashMode; } public void setHashMode(HashMode hashMode) { this.hashMode = hashMode; } public Set<String> getIgnoredFiles() { return ignoredFiles; } public void setIgnoredFiles(Set<String> ignoredFiles) { this.ignoredFiles = ignoredFiles; } public List<FileState> getFileStates() { return fileStates; } public ModificationCounts getModificationCounts() { return modificationCounts; } public void setModificationCounts(ModificationCounts modificationCounts) { this.modificationCounts = modificationCounts; } public CommitDetails getCommitDetails() { return commitDetails; } public void setCommitDetails(CommitDetails commitDetails) { this.commitDetails = commitDetails; } public String getStateHash() { return stateHash; } public void setStateHash(String stateHash) { this.stateHash = stateHash; } public String hashState() { HashFunction hashFunction = Hashing.sha512(); Hasher hasher = hashFunction.newHasher(Constants._10_MB); hashObject(hasher); HashCode hash = hasher.hash(); return Ascii85Util.encode(hash.asBytes()); } @Override public boolean equals(Object other) { if (this == other) { return true; } if (other == null || !(other instanceof State)) { return false; } State state = (State) other; return Objects.equals(this.modelVersion, state.modelVersion) && Objects.equals(this.timestamp, state.timestamp) && Objects.equals(this.comment, state.comment) && Objects.equals(this.fileCount, state.fileCount) && Objects.equals(this.filesContentLength, state.filesContentLength) && Objects.equals(this.hashMode, state.hashMode) && Objects.equals(this.ignoredFiles, state.ignoredFiles) && Objects.equals(this.fileStates, state.fileStates); } @Override public int hashCode() { return Objects.hash(modelVersion, timestamp, comment, fileCount, filesContentLength, hashMode, ignoredFiles, fileStates); } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("modelVersion", modelVersion) .add("timestamp", timestamp) .add("comment", comment) .add("fileCount", fileCount) .add("filesContentLength", filesContentLength) .add("hashMode", hashMode) .add("ignoredFiles", ignoredFiles) .add("fileStates", fileStates) .toString(); } @Override public void hashObject(Hasher hasher) { hasher .putString("State", Charsets.UTF_8) .putChar(HASH_FIELD_SEPARATOR) .putString(modelVersion, Charsets.UTF_8) .putChar(HASH_FIELD_SEPARATOR) .putLong(timestamp) .putChar(HASH_FIELD_SEPARATOR) .putString(comment, Charsets.UTF_8) .putChar(HASH_FIELD_SEPARATOR) .putInt(fileCount) .putChar(HASH_FIELD_SEPARATOR) .putLong(filesContentLength) .putChar(HASH_FIELD_SEPARATOR) .putString(hashMode.name(), Charsets.UTF_8); hasher.putChar(HASH_OBJECT_SEPARATOR); for (String ignoredFile : ignoredFiles) { hasher .putString(ignoredFile, Charsets.UTF_8) .putChar(HASH_OBJECT_SEPARATOR); } hasher.putChar(HASH_OBJECT_SEPARATOR); for (FileState fileState : fileStates) { fileState.hashObject(hasher); hasher.putChar(HASH_OBJECT_SEPARATOR); } } @Override public State clone() { return CLONER.deepClone(this); } }