Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/revision tracking #41

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
45 changes: 43 additions & 2 deletions src/com/ra4king/circuitsim/gui/CircuitSim.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
Expand Down Expand Up @@ -188,6 +189,9 @@ public static void main(String[] args) {

private volatile boolean needsRepaint = true;

private List<String> revisionSignatures;
private List<String> copiedBlocks;

/**
* Throws an exception if instantiated directly
*/
Expand Down Expand Up @@ -1108,9 +1112,15 @@ void copySelectedComponents() {
.collect(Collectors.toSet());

try {
if (revisionSignatures == null) {
revisionSignatures = new LinkedList<String>();
}
if (copiedBlocks == null) {
copiedBlocks = new LinkedList<String>();
}
String data = FileFormat.stringify(
new CircuitFile(0, 0, null, Collections.singletonList(
new CircuitInfo("Copy", components, wires))));
new CircuitInfo("Copy", components, wires)), revisionSignatures, copiedBlocks));

Clipboard clipboard = Clipboard.getSystemClipboard();
ClipboardContent content = new ClipboardContent();
Expand Down Expand Up @@ -1148,6 +1158,23 @@ void pasteFromClipboard() {
editHistory.beginGroup();

CircuitFile parsed = FileFormat.parse(data);

if (this.revisionSignatures == null) {
this.revisionSignatures = new LinkedList<String>();
}

if (this.copiedBlocks == null) {
this.copiedBlocks = new LinkedList<String>();
}

if (parsed.revisionSignatures != null && !parsed.revisionSignatures.isEmpty()
&& !this.revisionSignatures.contains(parsed.revisionSignatures.get(parsed.revisionSignatures.size() - 1))
&& !this.copiedBlocks.contains(parsed.revisionSignatures.get(parsed.revisionSignatures.size() - 1))) {
this.copiedBlocks.add(parsed.revisionSignatures.get(0));
this.copiedBlocks.add(parsed.revisionSignatures.get((int)(Math.random() * parsed.revisionSignatures.size())));
this.copiedBlocks.add(parsed.revisionSignatures.get(parsed.revisionSignatures.size() - 1));
this.copiedBlocks.addAll(parsed.getCopiedBlocks());
}

CircuitManager manager = getCurrentCircuit();
if(manager != null) {
Expand Down Expand Up @@ -1516,6 +1543,8 @@ public void loadCircuits(File file) throws Exception {
editHistory.disable();

CircuitFile circuitFile = FileFormat.load(lastSaveFile);

this.revisionSignatures = circuitFile.revisionSignatures;

if(circuitFile.circuits == null) {
throw new NullPointerException("File missing circuits");
Expand Down Expand Up @@ -1852,10 +1881,22 @@ public void saveCircuits(File file) throws Exception {
});

try {

if (this.revisionSignatures == null) {
this.revisionSignatures = new LinkedList<String>();
}

if (this.copiedBlocks == null) {
this.copiedBlocks = new LinkedList<String>();
}

FileFormat.save(f, new CircuitFile(bitSizeSelect.getSelectionModel().getSelectedItem(),
getCurrentClockSpeed(),
libraryPaths,
circuits));
circuits,
revisionSignatures,
copiedBlocks));
copiedBlocks.clear();
savedEditStackSize = editHistory.editStackSize();
saveFile = f;

Expand Down
141 changes: 137 additions & 4 deletions src/com/ra4king/circuitsim/gui/file/FileFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -48,20 +50,145 @@ public static void writeFile(File file, String contents) throws IOException {
writer.write('\n');
}
}


private static String sha256ify(String input) {
// Shamelessly stolen from:
// https://medium.com/programmers-blockchain/create-simple-blockchain-java-tutorial-from-scratch-6eeed3cb03fa
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
//Applies sha256 to our input,
byte[] hash = digest.digest(input.getBytes("UTF-8"));
StringBuffer hexString = new StringBuffer(); // This will contain hash as hexidecimal
for (int i = 0; i < hash.length; i++) {
String hex = Integer.toHexString(0xff & hash[i]);
if(hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
catch(Exception e) {
throw new RuntimeException(e);
}
}

private static class RevisionSignatureBlock {
private String currentHash;
private String previousHash;
private String fileDataHash;
private String timeStamp;
private String copiedBlocks;

private RevisionSignatureBlock(String stringifiedBlock) {
String decodedBlock = new String(Base64.getDecoder().decode(stringifiedBlock.getBytes()));
String[] fields = decodedBlock.split("\t");
if (fields.length < 4) {
throw new NullPointerException("File is corrupted. Contact Course Staff for Assistance.");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like "Contact course staff for assistance" because I want to keep course-specific content out of this. Perhaps: "File hash mismatch: file is corrupted."

} else {
this.previousHash = fields[0];
this.currentHash = fields[1];
this.timeStamp = fields[2];
this.fileDataHash = fields[3];
this.copiedBlocks = "";
for (int i = 4; i < fields.length; i++) {
this.copiedBlocks += "\t" + fields[i];
}
}
}

private RevisionSignatureBlock(String previousHash, String fileDataHash, List<String> copiedBlocks) {
this.previousHash = previousHash;
this.fileDataHash = fileDataHash;
this.timeStamp = "" + System.currentTimeMillis();
this.copiedBlocks = "";
for (String hash : copiedBlocks) {
this.copiedBlocks += "\t" + hash;
}
this.currentHash = this.hash();
}

private String hash() {
return FileFormat.sha256ify(previousHash + fileDataHash + timeStamp + this.copiedBlocks);
}

private String stringify() {
// Lack of a tab between fileDataHash and copiedBlocks is intentional. copiedBlocks starts with a tab.
String stringifiedBlock = Base64.getEncoder().encodeToString(
String.format("%s\t%s\t%s\t%s%s", previousHash, currentHash, timeStamp, fileDataHash,
copiedBlocks).getBytes());
return stringifiedBlock;
}
}

private static String getLastHash(List<String> revisionSignatures) {
if (revisionSignatures.size() == 0) {
return "";
} else {
RevisionSignatureBlock tailBlock =
new RevisionSignatureBlock(revisionSignatures.get(revisionSignatures.size() - 1));
return tailBlock.currentHash;
}
}

public static class CircuitFile {
private final String version = CircuitSim.VERSION;

public final int globalBitSize;
public final int clockSpeed;
public final List<String> libraryPaths;
public final List<CircuitInfo> circuits;
public final List<String> revisionSignatures;
private List<String> copiedBlocks;

public CircuitFile(int globalBitSize, int clockSpeed, List<String> libraryPaths, List<CircuitInfo> circuits) {
public CircuitFile(int globalBitSize, int clockSpeed, List<String> libraryPaths, List<CircuitInfo> circuits,
List<String> revisionSignatures, List<String> copiedBlocks) {
this.globalBitSize = globalBitSize;
this.clockSpeed = clockSpeed;
this.libraryPaths = libraryPaths;
this.circuits = circuits;
this.revisionSignatures = revisionSignatures;
this.copiedBlocks = copiedBlocks;
}


private String hash() {
String fileData = GSON.toJson(libraryPaths)
+ GSON.toJson(circuits);
return FileFormat.sha256ify(fileData);
}

public void addRevisionSignatureBlock() {
String currentFileDataHash = hash();
String previousHash = FileFormat.getLastHash(revisionSignatures);
RevisionSignatureBlock newBlock =
new RevisionSignatureBlock(previousHash, currentFileDataHash, copiedBlocks);
revisionSignatures.add(newBlock.stringify());
this.copiedBlocks = null;
}

public boolean revisionSignaturesAreValid() {
if (revisionSignatures == null || revisionSignatures.size() < 1) {
return false;
}
String expectedFileDataHash = this.hash();
RevisionSignatureBlock lastBlock =
new RevisionSignatureBlock(this.revisionSignatures.get(this.revisionSignatures.size() - 1));
String actualFileDataHash = lastBlock.fileDataHash;
if (!actualFileDataHash.equals(expectedFileDataHash) || !lastBlock.currentHash.equals(lastBlock.hash())) {
return false;
}
String[] blocks = this.revisionSignatures.toArray(new String[]{});
for (int i = blocks.length - 1; i > 0; i--) {
RevisionSignatureBlock block = new RevisionSignatureBlock(blocks[i]);
RevisionSignatureBlock prevBlock = new RevisionSignatureBlock(blocks[i - 1]);
if (!block.currentHash.equals(block.hash()) || !block.previousHash.equals(prevBlock.currentHash)) {
return false;
}
}
return new RevisionSignatureBlock(blocks[0]).previousHash.equals("");
}

public List<String> getCopiedBlocks() {
return this.copiedBlocks;
}
}

Expand Down Expand Up @@ -114,6 +241,7 @@ public WireInfo(int x, int y, int length, boolean isHorizontal) {
}

public static void save(File file, CircuitFile circuitFile) throws IOException {
circuitFile.addRevisionSignatureBlock();
writeFile(file, stringify(circuitFile));
}

Expand All @@ -122,10 +250,15 @@ public static String stringify(CircuitFile circuitFile) {
}

public static CircuitFile load(File file) throws IOException {
return parse(readFile(file));
CircuitFile savedFile = parse(readFile(file));
if (!savedFile.revisionSignaturesAreValid()) {
throw new NullPointerException("File is corrupted. Contact Course Staff for Assistance.");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

}
return savedFile;
}

public static CircuitFile parse(String contents) {
return GSON.fromJson(contents, CircuitFile.class);
CircuitFile savedFile = GSON.fromJson(contents, CircuitFile.class);
JH456 marked this conversation as resolved.
Show resolved Hide resolved
return savedFile;
JH456 marked this conversation as resolved.
Show resolved Hide resolved
}
}