Port metadata code to Kotlin

This commit is contained in:
comp500 2019-12-19 21:11:47 +00:00
parent ecaab219c2
commit 0770029dc6
18 changed files with 454 additions and 521 deletions

View File

@ -2,6 +2,7 @@ package link.infra.packwiz.installer;
import link.infra.packwiz.installer.metadata.IndexFile; import link.infra.packwiz.installer.metadata.IndexFile;
import link.infra.packwiz.installer.metadata.ManifestFile; import link.infra.packwiz.installer.metadata.ManifestFile;
import link.infra.packwiz.installer.metadata.ModFile;
import link.infra.packwiz.installer.metadata.SpaceSafeURI; import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource; import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
import link.infra.packwiz.installer.metadata.hash.Hash; import link.infra.packwiz.installer.metadata.hash.Hash;
@ -19,6 +20,7 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
class DownloadTask implements IOptionDetails, IExceptionDetails { class DownloadTask implements IOptionDetails, IExceptionDetails {
final IndexFile.File metadata; final IndexFile.File metadata;
@ -33,8 +35,8 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
private DownloadTask(IndexFile.File metadata, String defaultFormat, UpdateManager.Options.Side downloadSide) { private DownloadTask(IndexFile.File metadata, String defaultFormat, UpdateManager.Options.Side downloadSide) {
this.metadata = metadata; this.metadata = metadata;
if (metadata.hashFormat == null || metadata.hashFormat.length() == 0) { if (metadata.getHashFormat() == null || metadata.getHashFormat().length() == 0) {
metadata.hashFormat = defaultFormat; metadata.setHashFormat(defaultFormat);
} }
this.downloadSide = downloadSide; this.downloadSide = downloadSide;
} }
@ -56,18 +58,18 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
if (!invalidated) { if (!invalidated) {
Hash currHash; Hash currHash;
try { try {
currHash = HashUtils.getHash(metadata.hashFormat, metadata.hash); currHash = HashUtils.getHash(Objects.requireNonNull(metadata.getHashFormat()), Objects.requireNonNull(metadata.getHash()));
} catch (Exception e) { } catch (Exception e) {
failure = e; failure = e;
return; return;
} }
if (currHash != null && currHash.equals(cachedFile.hash)) { if (currHash.equals(cachedFile.getHash())) {
// Already up to date // Already up to date
alreadyUpToDate = true; alreadyUpToDate = true;
metadataRequired = false; metadataRequired = false;
} }
} }
if (cachedFile.isOptional) { if (cachedFile.isOptional()) {
// Because option selection dialog might set this task to true/false, metadata is always needed to download // Because option selection dialog might set this task to true/false, metadata is always needed to download
// the file, and to show the description and name // the file, and to show the description and name
metadataRequired = true; metadataRequired = true;
@ -83,21 +85,23 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
failure = e; failure = e;
return; return;
} }
if (metadata.linkedFile != null) { ModFile linkedFile = metadata.getLinkedFile();
if (metadata.linkedFile.option != null) { if (linkedFile != null) {
if (metadata.linkedFile.option.optional) { ModFile.Option option = linkedFile.getOption();
if (cachedFile.isOptional) { if (option != null) {
if (option.getOptional()) {
if (cachedFile.isOptional()) {
// isOptional didn't change // isOptional didn't change
newOptional = false; newOptional = false;
} else { } else {
// isOptional false -> true, set option to it's default value // isOptional false -> true, set option to it's default value
// TODO: preserve previous option value, somehow?? // TODO: preserve previous option value, somehow??
cachedFile.optionValue = this.metadata.linkedFile.option.defaultValue; cachedFile.setOptionValue(option.getDefaultValue());
} }
} }
} }
cachedFile.isOptional = isOptional(); cachedFile.setOptional(isOptional());
cachedFile.onlyOtherSide = !correctSide(); cachedFile.setOnlyOtherSide(!correctSide());
} }
} }
} }
@ -106,24 +110,24 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
if (failure != null) return; if (failure != null) return;
// Ensure it is removed // Ensure it is removed
if (!cachedFile.optionValue || !correctSide()) { if (!cachedFile.getOptionValue() || !correctSide()) {
if (cachedFile.cachedLocation == null) return; if (cachedFile.getCachedLocation() == null) return;
try { try {
Files.deleteIfExists(Paths.get(packFolder, cachedFile.cachedLocation)); Files.deleteIfExists(Paths.get(packFolder, cachedFile.getCachedLocation()));
} catch (IOException e) { } catch (IOException e) {
// TODO: how much of a problem is this? use log4j/other log library to show warning? // TODO: how much of a problem is this? use log4j/other log library to show warning?
e.printStackTrace(); e.printStackTrace();
} }
cachedFile.cachedLocation = null; cachedFile.setCachedLocation(null);
return; return;
} }
if (alreadyUpToDate) return; if (alreadyUpToDate) return;
Path destPath = Paths.get(packFolder, metadata.getDestURI().toString()); Path destPath = Paths.get(packFolder, Objects.requireNonNull(metadata.getDestURI()).toString());
// Don't update files marked with preserve if they already exist on disk // Don't update files marked with preserve if they already exist on disk
if (metadata.preserve) { if (metadata.getPreserve()) {
if (destPath.toFile().exists()) { if (destPath.toFile().exists()) {
return; return;
} }
@ -132,15 +136,17 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
try { try {
Hash hash; Hash hash;
String fileHashFormat; String fileHashFormat;
if (metadata.linkedFile != null) { ModFile linkedFile = metadata.getLinkedFile();
hash = metadata.linkedFile.getHash(); if (linkedFile != null) {
fileHashFormat = metadata.linkedFile.download.hashFormat; hash = linkedFile.getHash();
fileHashFormat = Objects.requireNonNull(linkedFile.getDownload()).getHashFormat();
} else { } else {
hash = metadata.getHash(); hash = metadata.getHashObj();
fileHashFormat = metadata.hashFormat; fileHashFormat = metadata.getHashFormat();
} }
Source src = metadata.getSource(indexUri); Source src = metadata.getSource(indexUri);
assert fileHashFormat != null;
GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src); GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
Buffer data = new Buffer(); Buffer data = new Buffer();
Okio.buffer(fileSource).readAll(data); Okio.buffer(fileSource).readAll(data);
@ -156,9 +162,9 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
failure = new Exception("Hash invalid!"); failure = new Exception("Hash invalid!");
} }
if (cachedFile.cachedLocation != null && !destPath.equals(Paths.get(packFolder, cachedFile.cachedLocation))) { if (cachedFile.getCachedLocation() != null && !destPath.equals(Paths.get(packFolder, cachedFile.getCachedLocation()))) {
// Delete old file if location changes // Delete old file if location changes
Files.delete(Paths.get(packFolder, cachedFile.cachedLocation)); Files.delete(Paths.get(packFolder, cachedFile.getCachedLocation()));
} }
} catch (Exception e) { } catch (Exception e) {
failure = e; failure = e;
@ -169,16 +175,16 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
} }
// Update the manifest file // Update the manifest file
try { try {
cachedFile.hash = metadata.getHash(); cachedFile.setHash(metadata.getHashObj());
} catch (Exception e) { } catch (Exception e) {
failure = e; failure = e;
return; return;
} }
cachedFile.isOptional = isOptional(); cachedFile.setOptional(isOptional());
cachedFile.cachedLocation = metadata.getDestURI().toString(); cachedFile.setCachedLocation(metadata.getDestURI().toString());
if (metadata.linkedFile != null) { if (metadata.getLinkedFile() != null) {
try { try {
cachedFile.linkedFileHash = metadata.linkedFile.getHash(); cachedFile.setLinkedFileHash(metadata.getLinkedFile().getHash());
} catch (Exception e) { } catch (Exception e) {
failure = e; failure = e;
} }
@ -191,8 +197,8 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
} }
boolean isOptional() { boolean isOptional() {
if (metadata.linkedFile != null) { if (metadata.getLinkedFile() != null) {
return metadata.linkedFile.isOptional(); return metadata.getLinkedFile().isOptional();
} }
return false; return false;
} }
@ -202,8 +208,8 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
} }
boolean correctSide() { boolean correctSide() {
if (metadata.linkedFile != null) { if (metadata.getLinkedFile() != null && metadata.getLinkedFile().getSide() != null) {
return metadata.linkedFile.side.hasSide(downloadSide); return metadata.getLinkedFile().getSide().hasSide(downloadSide);
} }
return true; return true;
} }
@ -214,28 +220,28 @@ class DownloadTask implements IOptionDetails, IExceptionDetails {
@Override @Override
public boolean getOptionValue() { public boolean getOptionValue() {
return cachedFile.optionValue; return cachedFile.getOptionValue();
} }
@Override @Override
public String getOptionDescription() { public String getOptionDescription() {
if (metadata.linkedFile != null) { if (metadata.getLinkedFile() != null && metadata.getLinkedFile().getOption() != null) {
return metadata.linkedFile.option.description; return metadata.getLinkedFile().getOption().getDescription();
} }
return null; return null;
} }
public void setOptionValue(boolean value) { public void setOptionValue(boolean value) {
if (value && !cachedFile.optionValue) { if (value && !cachedFile.getOptionValue()) {
// Ensure that an update is done if it changes from false to true, or from true to false // Ensure that an update is done if it changes from false to true, or from true to false
alreadyUpToDate = false; alreadyUpToDate = false;
} }
cachedFile.optionValue = value; cachedFile.setOptionValue(value);
} }
static List<DownloadTask> createTasksFromIndex(IndexFile index, String defaultFormat, UpdateManager.Options.Side downloadSide) { static List<DownloadTask> createTasksFromIndex(IndexFile index, String defaultFormat, UpdateManager.Options.Side downloadSide) {
ArrayList<DownloadTask> tasks = new ArrayList<>(); ArrayList<DownloadTask> tasks = new ArrayList<>();
for (IndexFile.File file : index.files) { for (IndexFile.File file : Objects.requireNonNull(index.getFiles())) {
tasks.add(new DownloadTask(file, defaultFormat, downloadSide)); tasks.add(new DownloadTask(file, defaultFormat, downloadSide));
} }
return tasks; return tasks;

View File

@ -151,17 +151,17 @@ public class UpdateManager {
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked // Invalidation checking must be done here, as it must happen before pack/index hashes are checked
List<SpaceSafeURI> invalidatedUris = new ArrayList<>(); List<SpaceSafeURI> invalidatedUris = new ArrayList<>();
if (manifest.cachedFiles != null) { if (manifest.getCachedFiles() != null) {
for (Map.Entry<SpaceSafeURI, ManifestFile.File> entry : manifest.cachedFiles.entrySet()) { for (Map.Entry<SpaceSafeURI, ManifestFile.File> entry : manifest.getCachedFiles().entrySet()) {
// ignore onlyOtherSide files // ignore onlyOtherSide files
if (entry.getValue().onlyOtherSide) { if (entry.getValue().getOnlyOtherSide()) {
continue; continue;
} }
boolean invalid = false; boolean invalid = false;
// if isn't optional, or is optional but optionValue == true // if isn't optional, or is optional but optionValue == true
if (!entry.getValue().isOptional || entry.getValue().optionValue) { if (!entry.getValue().isOptional() || entry.getValue().getOptionValue()) {
if (entry.getValue().cachedLocation != null) { if (entry.getValue().getCachedLocation() != null) {
if (!Paths.get(opts.packFolder, entry.getValue().cachedLocation).toFile().exists()) { if (!Paths.get(opts.packFolder, entry.getValue().getCachedLocation()).toFile().exists()) {
invalid = true; invalid = true;
} }
} else { } else {
@ -177,7 +177,7 @@ public class UpdateManager {
} }
} }
if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash) && invalidatedUris.isEmpty()) { if (manifest.getPackFileHash() != null && packFileSource.hashIsEqual(manifest.getPackFileHash()) && invalidatedUris.isEmpty()) {
System.out.println("Modpack is already up to date!"); System.out.println("Modpack is already up to date!");
// todo: --force? // todo: --force?
if (!stateHandler.getOptionsButton()) { if (!stateHandler.getOptionsButton()) {
@ -185,7 +185,7 @@ public class UpdateManager {
} }
} }
System.out.println("Modpack name: " + pf.name); System.out.println("Modpack name: " + pf.getName());
if (stateHandler.getCancelButton()) { if (stateHandler.getCancelButton()) {
showCancellationDialog(); showCancellationDialog();
@ -194,8 +194,8 @@ public class UpdateManager {
try { try {
// This is badly written, I'll probably heavily refactor it at some point // This is badly written, I'll probably heavily refactor it at some point
processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file), processIndex(HandlerManager.getNewLoc(opts.downloadURI, Objects.requireNonNull(pf.getIndex()).getFile()),
HashUtils.getHash(pf.index.hashFormat, pf.index.hash), pf.index.hashFormat, manifest, invalidatedUris); HashUtils.getHash(Objects.requireNonNull(pf.getIndex().getHashFormat()), Objects.requireNonNull(pf.getIndex().getHash())), pf.getIndex().getHashFormat(), manifest, invalidatedUris);
} catch (Exception e1) { } catch (Exception e1) {
ui.handleExceptionAndExit(e1); ui.handleExceptionAndExit(e1);
} }
@ -206,13 +206,13 @@ public class UpdateManager {
// If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later // If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later
if (errorsOccurred) { if (errorsOccurred) {
manifest.indexFileHash = null; manifest.setIndexFileHash(null);
manifest.packFileHash = null; manifest.setPackFileHash(null);
} else { } else {
manifest.packFileHash = packFileSource.getHash(); manifest.setPackFileHash(packFileSource.getHash());
} }
manifest.cachedSide = opts.side; manifest.setCachedSide(opts.side);
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) { try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
gson.toJson(manifest, writer); gson.toJson(manifest, writer);
} catch (IOException e) { } catch (IOException e) {
@ -227,13 +227,13 @@ public class UpdateManager {
} }
private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) { private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) {
if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash) && invalidatedUris.isEmpty()) { if (manifest.getIndexFileHash() != null && manifest.getIndexFileHash().equals(indexHash) && invalidatedUris.isEmpty()) {
System.out.println("Modpack files are already up to date!"); System.out.println("Modpack files are already up to date!");
if (!stateHandler.getOptionsButton()) { if (!stateHandler.getOptionsButton()) {
return; return;
} }
} }
manifest.indexFileHash = indexHash; manifest.setIndexFileHash(indexHash);
GeneralHashingSource indexFileSource; GeneralHashingSource indexFileSource;
try { try {
@ -264,33 +264,29 @@ public class UpdateManager {
return; return;
} }
if (manifest.cachedFiles == null) {
manifest.cachedFiles = new HashMap<>();
}
ui.submitProgress(new InstallProgress("Checking local files...")); ui.submitProgress(new InstallProgress("Checking local files..."));
Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.cachedFiles.entrySet().iterator(); Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.getCachedFiles().entrySet().iterator();
while (it.hasNext()) { while (it.hasNext()) {
Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next(); Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next();
if (entry.getValue().cachedLocation != null) { if (entry.getValue().getCachedLocation() != null) {
boolean alreadyDeleted = false; boolean alreadyDeleted = false;
// Delete if option value has been set to false // Delete if option value has been set to false
if (entry.getValue().isOptional && !entry.getValue().optionValue) { if (entry.getValue().isOptional() && !entry.getValue().getOptionValue()) {
try { try {
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation)); Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation()));
} catch (IOException e) { } catch (IOException e) {
// TODO: should this be shown to the user in some way? // TODO: should this be shown to the user in some way?
e.printStackTrace(); e.printStackTrace();
} }
// Set to null, as it doesn't exist anymore // Set to null, as it doesn't exist anymore
entry.getValue().cachedLocation = null; entry.getValue().setCachedLocation(null);
alreadyDeleted = true; alreadyDeleted = true;
} }
if (indexFile.files.stream().noneMatch(f -> f.file.equals(entry.getKey()))) { if (indexFile.getFiles().stream().noneMatch(f -> Objects.equals(f.getFile(), entry.getKey()))) {
// File has been removed from the index // File has been removed from the index
if (!alreadyDeleted) { if (!alreadyDeleted) {
try { try {
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation)); Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation()));
} catch (IOException e) { } catch (IOException e) {
// TODO: should this be shown to the user in some way? // TODO: should this be shown to the user in some way?
e.printStackTrace(); e.printStackTrace();
@ -308,14 +304,13 @@ public class UpdateManager {
ui.submitProgress(new InstallProgress("Comparing new files...")); ui.submitProgress(new InstallProgress("Comparing new files..."));
// TODO: progress bar? // TODO: progress bar?
if (indexFile.files == null || indexFile.files.size() == 0) { if (indexFile.getFiles().isEmpty()) {
System.out.println("Warning: Index is empty!"); System.out.println("Warning: Index is empty!");
indexFile.files = new ArrayList<>();
} }
List<DownloadTask> tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side); List<DownloadTask> tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.getHashFormat(), opts.side);
// If the side changes, invalidate EVERYTHING just in case // If the side changes, invalidate EVERYTHING just in case
// Might not be needed, but done just to be safe // Might not be needed, but done just to be safe
boolean invalidateAll = !opts.side.equals(manifest.cachedSide); boolean invalidateAll = !opts.side.equals(manifest.getCachedSide());
if (invalidateAll) { if (invalidateAll) {
System.out.println("Side changed, invalidating all mods"); System.out.println("Side changed, invalidating all mods");
} }
@ -323,10 +318,10 @@ public class UpdateManager {
// TODO: should linkedfile be checked as well? should this be done in the download section? // TODO: should linkedfile be checked as well? should this be done in the download section?
if (invalidateAll) { if (invalidateAll) {
f.invalidate(); f.invalidate();
} else if (invalidatedUris.contains(f.metadata.file)) { } else if (invalidatedUris.contains(f.metadata.getFile())) {
f.invalidate(); f.invalidate();
} }
ManifestFile.File file = manifest.cachedFiles.get(f.metadata.file); ManifestFile.File file = manifest.getCachedFiles().get(f.metadata.getFile());
if (file != null) { if (file != null) {
// Ensure the file can be reverted later if necessary - the DownloadTask modifies the file so if it fails we need the old version back // Ensure the file can be reverted later if necessary - the DownloadTask modifies the file so if it fails we need the old version back
file.backup(); file.backup();
@ -412,11 +407,11 @@ public class UpdateManager {
if (task.getException() != null) { if (task.getException() != null) {
ManifestFile.File file = task.cachedFile.getRevert(); ManifestFile.File file = task.cachedFile.getRevert();
if (file != null) { if (file != null) {
manifest.cachedFiles.putIfAbsent(task.metadata.file, file); manifest.getCachedFiles().putIfAbsent(task.metadata.getFile(), file);
} }
} else { } else {
// idiot, if it wasn't there in the first place it won't magically appear there // idiot, if it wasn't there in the first place it won't magically appear there
manifest.cachedFiles.putIfAbsent(task.metadata.file, task.cachedFile); manifest.getCachedFiles().putIfAbsent(task.metadata.getFile(), task.cachedFile);
} }
} }

View File

@ -1,29 +0,0 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
public class EfficientBooleanAdapter extends TypeAdapter<Boolean> {
@Override
public void write(JsonWriter out, Boolean value) throws IOException {
if (value == null || !value) {
out.nullValue();
return;
}
out.value(true);
}
@Override
public Boolean read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return false;
}
return in.nextBoolean();
}
}

View File

@ -1,105 +0,0 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.annotations.SerializedName;
import com.moandjiezana.toml.Toml;
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
import link.infra.packwiz.installer.metadata.hash.Hash;
import link.infra.packwiz.installer.metadata.hash.HashUtils;
import link.infra.packwiz.installer.request.HandlerManager;
import okio.Okio;
import okio.Source;
import java.nio.file.Paths;
import java.util.List;
public class IndexFile {
@SerializedName("hash-format")
public String hashFormat;
public List<File> files;
public static class File {
public SpaceSafeURI file;
@SerializedName("hash-format")
public String hashFormat;
public String hash;
public SpaceSafeURI alias;
public boolean metafile;
public boolean preserve;
public transient ModFile linkedFile;
public transient SpaceSafeURI linkedFileURI;
public void downloadMeta(IndexFile parentIndexFile, SpaceSafeURI indexUri) throws Exception {
if (!metafile) {
return;
}
if (hashFormat == null || hashFormat.length() == 0) {
hashFormat = parentIndexFile.hashFormat;
}
Hash fileHash = HashUtils.getHash(hashFormat, hash);
linkedFileURI = HandlerManager.getNewLoc(indexUri, file);
Source src = HandlerManager.getFileSource(linkedFileURI);
GeneralHashingSource fileStream = HashUtils.getHasher(hashFormat).getHashingSource(src);
linkedFile = new Toml().read(Okio.buffer(fileStream).inputStream()).to(ModFile.class);
if (!fileStream.hashIsEqual(fileHash)) {
throw new Exception("Invalid mod file hash");
}
}
public Source getSource(SpaceSafeURI indexUri) throws Exception {
if (metafile) {
if (linkedFile == null) {
throw new Exception("Linked file doesn't exist!");
}
return linkedFile.getSource(linkedFileURI);
} else {
SpaceSafeURI newLoc = HandlerManager.getNewLoc(indexUri, file);
if (newLoc == null) {
throw new Exception("Index file URI is invalid");
}
return HandlerManager.getFileSource(newLoc);
}
}
public Hash getHash() throws Exception {
if (hash == null) {
// TODO: should these be more specific exceptions (e.g. IndexFileException?!)
throw new Exception("Index file doesn't have a hash");
}
if (hashFormat == null) {
throw new Exception("Index file doesn't have a hash format");
}
return HashUtils.getHash(hashFormat, hash);
}
public String getName() {
if (metafile) {
if (linkedFile != null) {
if (linkedFile.name != null) {
return linkedFile.name;
} else if (linkedFile.filename != null) {
return linkedFile.filename;
}
}
}
if (file != null) {
return Paths.get(file.getPath()).getFileName().toString();
}
// TODO: throw some kind of exception?
return "Invalid file";
}
public SpaceSafeURI getDestURI() {
if (alias != null) {
return alias;
}
if (metafile && linkedFile != null) {
// TODO: URIs are bad
return file.resolve(linkedFile.filename);
} else {
return file;
}
}
}
}

View File

@ -1,45 +0,0 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.annotations.JsonAdapter;
import link.infra.packwiz.installer.UpdateManager;
import link.infra.packwiz.installer.metadata.hash.Hash;
import java.util.Map;
public class ManifestFile {
public Hash packFileHash = null;
public Hash indexFileHash = null;
public Map<SpaceSafeURI, File> cachedFiles;
// If the side changes, EVERYTHING invalidates. FUN!!!
public UpdateManager.Options.Side cachedSide = UpdateManager.Options.Side.CLIENT;
public static class File {
private transient File revert;
public Hash hash = null;
public Hash linkedFileHash = null;
public String cachedLocation = null;
@JsonAdapter(EfficientBooleanAdapter.class)
public boolean isOptional = false;
public boolean optionValue = true;
@JsonAdapter(EfficientBooleanAdapter.class)
public boolean onlyOtherSide = false;
// When an error occurs, the state needs to be reverted. To do this, I have a crude revert system.
public void backup() {
revert = new File();
revert.hash = hash;
revert.linkedFileHash = linkedFileHash;
revert.cachedLocation = cachedLocation;
revert.isOptional = isOptional;
revert.optionValue = optionValue;
revert.onlyOtherSide = onlyOtherSide;
}
public File getRevert() {
return revert;
}
}
}

View File

@ -1,70 +0,0 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.annotations.SerializedName;
import link.infra.packwiz.installer.UpdateManager.Options.Side;
import link.infra.packwiz.installer.metadata.hash.Hash;
import link.infra.packwiz.installer.metadata.hash.HashUtils;
import link.infra.packwiz.installer.request.HandlerManager;
import okio.Source;
import java.util.Map;
public class ModFile {
public String name;
public String filename;
public Side side;
public Download download;
public static class Download {
public SpaceSafeURI url;
@SerializedName("hash-format")
public String hashFormat;
public String hash;
}
public Map<String, Object> update;
public Option option;
public static class Option {
public boolean optional;
public String description;
@SerializedName("default")
public boolean defaultValue;
}
public Source getSource(SpaceSafeURI baseLoc) throws Exception {
if (download == null) {
throw new Exception("Metadata file doesn't have download");
}
if (download.url == null) {
throw new Exception("Metadata file doesn't have a download URI");
}
SpaceSafeURI newLoc = HandlerManager.getNewLoc(baseLoc, download.url);
if (newLoc == null) {
throw new Exception("Metadata file URI is invalid");
}
return HandlerManager.getFileSource(newLoc);
}
public Hash getHash() throws Exception {
if (download == null) {
throw new Exception("Metadata file doesn't have download");
}
if (download.hash == null) {
throw new Exception("Metadata file doesn't have a hash");
}
if (download.hashFormat == null) {
throw new Exception("Metadata file doesn't have a hash format");
}
return HashUtils.getHash(download.hashFormat, download.hash);
}
public boolean isOptional() {
if (option != null) {
return option.optional;
}
return false;
}
}

View File

@ -1,21 +0,0 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.annotations.SerializedName;
import java.util.Map;
public class PackFile {
public String name;
public IndexFileLoc index;
public static class IndexFileLoc {
public SpaceSafeURI file;
@SerializedName("hash-format")
public String hashFormat;
public String hash;
}
public Map<String, String> versions;
public Map<String, Object> client;
public Map<String, Object> server;
}

View File

@ -1,88 +0,0 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.annotations.JsonAdapter;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
// The world's worst URI wrapper
@JsonAdapter(SpaceSafeURIParser.class)
public class SpaceSafeURI implements Comparable<SpaceSafeURI>, Serializable {
private final URI u;
public SpaceSafeURI(String str) throws URISyntaxException {
u = new URI(str.replace(" ", "%20"));
}
public SpaceSafeURI(URI uri) {
this.u = uri;
}
public SpaceSafeURI(String scheme, String authority, String path, String query, String fragment) throws URISyntaxException {
// TODO: do all components need to be replaced?
scheme = scheme.replace(" ", "%20");
authority = authority.replace(" ", "%20");
path = path.replace(" ", "%20");
query = query.replace(" ", "%20");
fragment = fragment.replace(" ", "%20");
u = new URI(scheme, authority, path, query, fragment);
}
public String getPath() {
return u.getPath().replace("%20", " ");
}
public String toString() {
return u.toString().replace("%20", " ");
}
@SuppressWarnings("WeakerAccess")
public SpaceSafeURI resolve(String path) {
return new SpaceSafeURI(u.resolve(path.replace(" ", "%20")));
}
public SpaceSafeURI resolve(SpaceSafeURI loc) {
return new SpaceSafeURI(u.resolve(loc.u));
}
public SpaceSafeURI relativize(SpaceSafeURI loc) {
return new SpaceSafeURI(u.relativize(loc.u));
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SpaceSafeURI) {
return u.equals(((SpaceSafeURI) obj).u);
}
return false;
}
@Override
public int hashCode() {
return u.hashCode();
}
@Override
public int compareTo(SpaceSafeURI uri) {
return u.compareTo(uri.u);
}
public String getScheme() {
return u.getScheme();
}
public String getAuthority() {
return u.getAuthority();
}
public String getHost() {
return u.getHost();
}
public URL toURL() throws MalformedURLException {
return u.toURL();
}
}

View File

@ -1,29 +0,0 @@
package link.infra.packwiz.installer.metadata;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.net.URISyntaxException;
/**
* This class encodes spaces before parsing the URI, so the URI can actually be
* parsed.
*/
class SpaceSafeURIParser implements JsonDeserializer<SpaceSafeURI> {
@Override
public SpaceSafeURI deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
try {
return new SpaceSafeURI(json.getAsString());
} catch (URISyntaxException e) {
throw new JsonParseException("Failed to parse URI", e);
}
}
// TODO: replace this with a better solution?
}

View File

@ -1,59 +0,0 @@
package link.infra.packwiz.installer.request;
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub;
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP;
import okio.Source;
import java.util.ArrayList;
import java.util.List;
public abstract class HandlerManager {
private static List<IRequestHandler> handlers = new ArrayList<>();
static {
handlers.add(new RequestHandlerGithub());
handlers.add(new RequestHandlerHTTP());
}
public static SpaceSafeURI getNewLoc(SpaceSafeURI base, SpaceSafeURI loc) {
if (loc == null) return null;
if (base != null) {
loc = base.resolve(loc);
}
for (IRequestHandler handler : handlers) {
if (handler.matchesHandler(loc)) {
return handler.getNewLoc(loc);
}
}
return loc;
}
// TODO: What if files are read multiple times??
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
// Caching system? Copy from already downloaded files?
public static Source getFileSource(SpaceSafeURI loc) throws Exception {
for (IRequestHandler handler : handlers) {
if (handler.matchesHandler(loc)) {
Source src = handler.getFileSource(loc);
if (src == null) {
throw new Exception("Couldn't find URI: " + loc.toString());
} else {
return src;
}
}
}
// TODO: specialised exception classes??
throw new Exception("No handler available for URI: " + loc.toString());
}
// github toml resolution
// e.g. https://github.com/comp500/Demagnetize -> demagnetize.toml
// https://github.com/comp500/Demagnetize/blob/master/demagnetize.toml
// To handle "progress", just count tasks, rather than individual progress
// It'll look bad, especially for zip-based things, but it should work fine
}

View File

@ -0,0 +1,27 @@
package link.infra.packwiz.installer.metadata
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import java.io.IOException
class EfficientBooleanAdapter : TypeAdapter<Boolean?>() {
@Throws(IOException::class)
override fun write(out: JsonWriter, value: Boolean?) {
if (value == null || !value) {
out.nullValue()
return
}
out.value(true)
}
@Throws(IOException::class)
override fun read(reader: JsonReader): Boolean? {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return false
}
return reader.nextBoolean()
}
}

View File

@ -0,0 +1,98 @@
package link.infra.packwiz.installer.metadata
import com.google.gson.annotations.SerializedName
import com.moandjiezana.toml.Toml
import link.infra.packwiz.installer.metadata.hash.Hash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
import okio.Source
import okio.buffer
import java.nio.file.Paths
class IndexFile {
@SerializedName("hash-format")
var hashFormat: String = "sha-256"
var files: MutableList<File> = ArrayList()
class File {
var file: SpaceSafeURI? = null
@SerializedName("hash-format")
var hashFormat: String? = null
var hash: String? = null
var alias: SpaceSafeURI? = null
var metafile = false
var preserve = false
@Transient
var linkedFile: ModFile? = null
@Transient
var linkedFileURI: SpaceSafeURI? = null
@Throws(Exception::class)
fun downloadMeta(parentIndexFile: IndexFile, indexUri: SpaceSafeURI?) {
if (!metafile) {
return
}
if (hashFormat?.length ?: 0 == 0) {
hashFormat = parentIndexFile.hashFormat
}
// TODO: throw a proper exception instead of allowing NPE?
val fileHash = getHash(hashFormat!!, hash!!)
linkedFileURI = getNewLoc(indexUri, file)
val src = getFileSource(linkedFileURI!!)
val fileStream = getHasher(hashFormat!!).getHashingSource(src)
linkedFile = Toml().read(fileStream.buffer().inputStream()).to(ModFile::class.java)
if (!fileStream.hashIsEqual(fileHash)) {
throw Exception("Invalid mod file hash")
}
}
@Throws(Exception::class)
fun getSource(indexUri: SpaceSafeURI?): Source {
return if (metafile) {
if (linkedFile == null) {
throw Exception("Linked file doesn't exist!")
}
linkedFile!!.getSource(linkedFileURI)
} else {
val newLoc = getNewLoc(indexUri, file) ?: throw Exception("Index file URI is invalid")
getFileSource(newLoc)
}
}
@Throws(Exception::class)
fun getHashObj(): Hash {
if (hash == null) { // TODO: should these be more specific exceptions (e.g. IndexFileException?!)
throw Exception("Index file doesn't have a hash")
}
if (hashFormat == null) {
throw Exception("Index file doesn't have a hash format")
}
return getHash(hashFormat!!, hash!!)
}
// TODO: throw some kind of exception?
val name: String?
get() {
if (metafile) {
return linkedFile?.name ?: linkedFile?.filename
}
return file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
}
// TODO: URIs are bad
val destURI: SpaceSafeURI?
get() {
if (alias != null) {
return alias
}
return if (metafile && linkedFile != null) {
linkedFile?.filename?.let { file?.resolve(it) }
} else {
file
}
}
}
}

View File

@ -0,0 +1,44 @@
package link.infra.packwiz.installer.metadata
import com.google.gson.annotations.JsonAdapter
import link.infra.packwiz.installer.UpdateManager
import link.infra.packwiz.installer.metadata.hash.Hash
class ManifestFile {
var packFileHash: Hash? = null
var indexFileHash: Hash? = null
var cachedFiles: MutableMap<SpaceSafeURI, File> = HashMap()
// If the side changes, EVERYTHING invalidates. FUN!!!
var cachedSide = UpdateManager.Options.Side.CLIENT
// TODO: switch to Kotlin-friendly JSON/TOML libs?
class File {
@Transient
var revert: File? = null
private set
var hash: Hash? = null
var linkedFileHash: Hash? = null
var cachedLocation: String? = null
@JsonAdapter(EfficientBooleanAdapter::class)
var isOptional = false
var optionValue = true
@JsonAdapter(EfficientBooleanAdapter::class)
var onlyOtherSide = false
// When an error occurs, the state needs to be reverted. To do this, I have a crude revert system.
fun backup() {
revert = File().also {
it.hash = hash
it.linkedFileHash = linkedFileHash
it.cachedLocation = cachedLocation
it.isOptional = isOptional
it.optionValue = optionValue
it.onlyOtherSide = onlyOtherSide
}
}
}
}

View File

@ -0,0 +1,57 @@
package link.infra.packwiz.installer.metadata
import com.google.gson.annotations.SerializedName
import link.infra.packwiz.installer.UpdateManager
import link.infra.packwiz.installer.metadata.hash.Hash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
import okio.Source
class ModFile {
var name: String? = null
var filename: String? = null
var side: UpdateManager.Options.Side? = null
var download: Download? = null
class Download {
var url: SpaceSafeURI? = null
@SerializedName("hash-format")
var hashFormat: String? = null
var hash: String? = null
}
var update: Map<String, Any>? = null
var option: Option? = null
class Option {
var optional = false
var description: String? = null
@SerializedName("default")
var defaultValue = false
}
@Throws(Exception::class)
fun getSource(baseLoc: SpaceSafeURI?): Source {
download?.let {
if (it.url == null) {
throw Exception("Metadata file doesn't have a download URI")
}
val newLoc = getNewLoc(baseLoc, it.url) ?: throw Exception("Metadata file URI is invalid")
return getFileSource(newLoc)
} ?: throw Exception("Metadata file doesn't have download")
}
@get:Throws(Exception::class)
val hash: Hash
get() {
download?.let {
return getHash(
it.hashFormat ?: throw Exception("Metadata file doesn't have a hash format"),
it.hash ?: throw Exception("Metadata file doesn't have a hash")
)
} ?: throw Exception("Metadata file doesn't have download")
}
val isOptional: Boolean get() = option?.optional ?: false
}

View File

@ -0,0 +1,19 @@
package link.infra.packwiz.installer.metadata
import com.google.gson.annotations.SerializedName
class PackFile {
var name: String? = null
var index: IndexFileLoc? = null
class IndexFileLoc {
var file: SpaceSafeURI? = null
@SerializedName("hash-format")
var hashFormat: String? = null
var hash: String? = null
}
var versions: Map<String, String>? = null
var client: Map<String, Any>? = null
var server: Map<String, Any>? = null
}

View File

@ -0,0 +1,61 @@
package link.infra.packwiz.installer.metadata
import com.google.gson.annotations.JsonAdapter
import java.io.Serializable
import java.net.MalformedURLException
import java.net.URI
import java.net.URISyntaxException
import java.net.URL
// The world's worst URI wrapper
@JsonAdapter(SpaceSafeURIParser::class)
class SpaceSafeURI : Comparable<SpaceSafeURI>, Serializable {
private val u: URI
@Throws(URISyntaxException::class)
constructor(str: String) {
u = URI(str.replace(" ", "%20"))
}
constructor(uri: URI) {
u = uri
}
@Throws(URISyntaxException::class)
constructor(scheme: String?, authority: String?, path: String?, query: String?, fragment: String?) { // TODO: do all components need to be replaced?
u = URI(
scheme?.replace(" ", "%20"),
authority?.replace(" ", "%20"),
path?.replace(" ", "%20"),
query?.replace(" ", "%20"),
fragment?.replace(" ", "%20")
)
}
val path: String get() = u.path.replace("%20", " ")
override fun toString(): String = u.toString().replace("%20", " ")
fun resolve(path: String): SpaceSafeURI = SpaceSafeURI(u.resolve(path.replace(" ", "%20")))
fun resolve(loc: SpaceSafeURI): SpaceSafeURI = SpaceSafeURI(u.resolve(loc.u))
fun relativize(loc: SpaceSafeURI): SpaceSafeURI = SpaceSafeURI(u.relativize(loc.u))
override fun equals(other: Any?): Boolean {
return if (other is SpaceSafeURI) {
u == other.u
} else false
}
override fun hashCode() = u.hashCode()
override fun compareTo(other: SpaceSafeURI): Int = u.compareTo(other.u)
val scheme: String get() = u.scheme
val authority: String get() = u.authority
val host: String get() = u.host
@Throws(MalformedURLException::class)
fun toURL(): URL = u.toURL()
}

View File

@ -0,0 +1,25 @@
package link.infra.packwiz.installer.metadata
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
import java.net.URISyntaxException
/**
* This class encodes spaces before parsing the URI, so the URI can actually be
* parsed.
*/
internal class SpaceSafeURIParser : JsonDeserializer<SpaceSafeURI> {
@Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): SpaceSafeURI {
return try {
SpaceSafeURI(json.asString)
} catch (e: URISyntaxException) {
throw JsonParseException("Failed to parse URI", e)
}
}
// TODO: replace this with a better solution?
}

View File

@ -0,0 +1,47 @@
package link.infra.packwiz.installer.request
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP
import okio.Source
object HandlerManager {
private val handlers: List<IRequestHandler> = listOf(
RequestHandlerGithub(),
RequestHandlerHTTP()
)
@JvmStatic
fun getNewLoc(base: SpaceSafeURI?, loc: SpaceSafeURI?): SpaceSafeURI? {
if (loc == null) {
return null
}
val dest = base?.run { resolve(loc) } ?: loc
for (handler in handlers) with (handler) {
if (matchesHandler(dest)) {
return getNewLoc(dest)
}
}
return dest
}
// TODO: What if files are read multiple times??
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
// Caching system? Copy from already downloaded files?
@JvmStatic
@Throws(Exception::class)
fun getFileSource(loc: SpaceSafeURI): Source {
for (handler in handlers) {
if (handler.matchesHandler(loc)) {
return handler.getFileSource(loc) ?: throw Exception("Couldn't find URI: $loc")
}
}
throw Exception("No handler available for URI: $loc")
}
// TODO: github toml resolution?
// e.g. https://github.com/comp500/Demagnetize -> demagnetize.toml
// https://github.com/comp500/Demagnetize/blob/master/demagnetize.toml
}