package launcher.hasher;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import launcher.LauncherAPI;
import launcher.helper.IOHelper;
import launcher.helper.VerifyHelper;
import launcher.serialize.HInput;
import launcher.serialize.HOutput;
import launcher.serialize.stream.EnumSerializer;
public final class HashedDir extends HashedEntry {
private final Map<String, HashedEntry> map = new HashMap<>(32);
@LauncherAPI
public HashedDir() {
}
@LauncherAPI
public HashedDir(Path dir, FileNameMatcher matcher) throws IOException {
IOHelper.walk(dir, new HashFileVisitor(dir, matcher), true);
}
@LauncherAPI
public HashedDir(HInput input) throws IOException {
int entriesCount = input.readLength(0);
for (int i = 0; i < entriesCount; i++) {
String name = IOHelper.verifyFileName(input.readString(255));
// Read entry
HashedEntry entry;
Type type = Type.read(input);
switch (type) {
case FILE:
entry = new HashedFile(input);
break;
case DIR:
entry = new HashedDir(input);
break;
default:
throw new AssertionError("Unsupported hashed entry type: " + type.name());
}
// Try add entry to map
VerifyHelper.putIfAbsent(map, name, entry, String.format("Duplicate dir entry: '%s'", name));
}
}
@Override
public Type getType() {
return Type.DIR;
}
@Override
public long size() {
return map.values().stream().mapToLong(HashedEntry::size).sum();
}
@Override
public void write(HOutput output) throws IOException {
Set<Map.Entry<String, HashedEntry>> entries = map.entrySet();
output.writeLength(entries.size(), 0);
for (Map.Entry<String, HashedEntry> mapEntry : entries) {
output.writeString(mapEntry.getKey(), 255);
// Write hashed entry
HashedEntry entry = mapEntry.getValue();
EnumSerializer.write(output, entry.getType());
entry.write(output);
}
}
@LauncherAPI
public Diff diff(HashedDir other, FileNameMatcher matcher) {
HashedDir mismatch = sideDiff(other, matcher, new LinkedList<>(), true);
HashedDir extra = other.sideDiff(this, matcher, new LinkedList<>(), false);
return new Diff(mismatch, extra);
}
@LauncherAPI
public HashedEntry getEntry(String name) {
return map.get(name);
}
@LauncherAPI
public boolean isEmpty() {
return map.isEmpty();
}
@LauncherAPI
public Map<String, HashedEntry> map() {
return Collections.unmodifiableMap(map);
}
@LauncherAPI
public HashedEntry resolve(Iterable<String> path) {
HashedEntry current = this;
for (String pathEntry : path) {
if (current instanceof HashedDir) {
current = ((HashedDir) current).map.get(pathEntry);
continue;
}
return null;
}
return current;
}
private HashedDir sideDiff(HashedDir other, FileNameMatcher matcher, Deque<String> path, boolean mismatchList) {
HashedDir diff = new HashedDir();
for (Map.Entry<String, HashedEntry> mapEntry : map.entrySet()) {
String name = mapEntry.getKey();
HashedEntry entry = mapEntry.getValue();
path.add(name);
// Should update?
boolean shouldUpdate = matcher == null || matcher.shouldUpdate(path);
// Not found or of different type
Type type = entry.getType();
HashedEntry otherEntry = other.map.get(name);
if (otherEntry == null || otherEntry.getType() != type) {
if (shouldUpdate || mismatchList && otherEntry == null) {
diff.map.put(name, entry);
// Should be deleted!
if (!mismatchList) {
entry.flag = true;
}
}
path.removeLast();
continue;
}
// Compare entries based on type
switch (type) {
case FILE:
HashedFile file = (HashedFile) entry;
HashedFile otherFile = (HashedFile) otherEntry;
if (mismatchList && shouldUpdate && !file.isSame(otherFile)) {
diff.map.put(name, entry);
}
break;
case DIR:
HashedDir dir = (HashedDir) entry;
HashedDir otherDir = (HashedDir) otherEntry;
if (mismatchList || shouldUpdate) { // Maybe isn't need to go deeper?
HashedDir mismatch = dir.sideDiff(otherDir, matcher, path, mismatchList);
if (!mismatch.isEmpty()) {
diff.map.put(name, mismatch);
}
}
break;
default:
throw new AssertionError("Unsupported hashed entry type: " + type.name());
}
// Remove this path entry
path.removeLast();
}
return diff;
}
private final class HashFileVisitor extends SimpleFileVisitor<Path> {
private final Path dir;
private final FileNameMatcher matcher;
private final Deque<String> path = new LinkedList<>();
private final Deque<HashedDir> stack = new LinkedList<>();
// State
private HashedDir current = HashedDir.this;
private HashFileVisitor(Path dir, FileNameMatcher matcher) {
this.dir = dir;
this.matcher = matcher;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
FileVisitResult result = super.postVisitDirectory(dir, exc);
if (this.dir.equals(dir)) {
return result;
}
// Add directory to parent
HashedDir parent = stack.removeLast();
parent.map.put(path.removeLast(), current);
current = parent;
// We're done
return result;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
FileVisitResult result = super.preVisitDirectory(dir, attrs);
if (this.dir.equals(dir)) {
return result;
}
// Add child
stack.add(current);
current = new HashedDir();
path.add(IOHelper.getFileName(dir));
// We're done
return result;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
path.add(IOHelper.getFileName(file));
// Add file (may be unhashed, if exclusion)
boolean hash = matcher == null || matcher.shouldUpdate(path);
current.map.put(path.removeLast(), new HashedFile(file, attrs.size(), hash));
return super.visitFile(file, attrs);
}
}
public static final class Diff {
@LauncherAPI public final HashedDir mismatch;
@LauncherAPI public final HashedDir extra;
private Diff(HashedDir mismatch, HashedDir extra) {
this.mismatch = mismatch;
this.extra = extra;
}
@LauncherAPI
public boolean isSame() {
return mismatch.isEmpty() && extra.isEmpty();
}
}
}