diff --git a/src/main/java/link/infra/packwiz/installer/DownloadTask.java b/src/main/java/link/infra/packwiz/installer/DownloadTask.java index 417a63e..0771379 100644 --- a/src/main/java/link/infra/packwiz/installer/DownloadTask.java +++ b/src/main/java/link/infra/packwiz/installer/DownloadTask.java @@ -2,6 +2,7 @@ package link.infra.packwiz.installer; import link.infra.packwiz.installer.metadata.IndexFile; 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.hash.GeneralHashingSource; import link.infra.packwiz.installer.metadata.hash.Hash; @@ -19,6 +20,7 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; +import java.util.Objects; class DownloadTask implements IOptionDetails, IExceptionDetails { final IndexFile.File metadata; @@ -33,8 +35,8 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { private DownloadTask(IndexFile.File metadata, String defaultFormat, UpdateManager.Options.Side downloadSide) { this.metadata = metadata; - if (metadata.hashFormat == null || metadata.hashFormat.length() == 0) { - metadata.hashFormat = defaultFormat; + if (metadata.getHashFormat() == null || metadata.getHashFormat().length() == 0) { + metadata.setHashFormat(defaultFormat); } this.downloadSide = downloadSide; } @@ -56,18 +58,18 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { if (!invalidated) { Hash currHash; try { - currHash = HashUtils.getHash(metadata.hashFormat, metadata.hash); + currHash = HashUtils.getHash(Objects.requireNonNull(metadata.getHashFormat()), Objects.requireNonNull(metadata.getHash())); } catch (Exception e) { failure = e; return; } - if (currHash != null && currHash.equals(cachedFile.hash)) { + if (currHash.equals(cachedFile.getHash())) { // Already up to date alreadyUpToDate = true; 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 // the file, and to show the description and name metadataRequired = true; @@ -83,21 +85,23 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { failure = e; return; } - if (metadata.linkedFile != null) { - if (metadata.linkedFile.option != null) { - if (metadata.linkedFile.option.optional) { - if (cachedFile.isOptional) { + ModFile linkedFile = metadata.getLinkedFile(); + if (linkedFile != null) { + ModFile.Option option = linkedFile.getOption(); + if (option != null) { + if (option.getOptional()) { + if (cachedFile.isOptional()) { // isOptional didn't change newOptional = false; } else { // isOptional false -> true, set option to it's default value // TODO: preserve previous option value, somehow?? - cachedFile.optionValue = this.metadata.linkedFile.option.defaultValue; + cachedFile.setOptionValue(option.getDefaultValue()); } } } - cachedFile.isOptional = isOptional(); - cachedFile.onlyOtherSide = !correctSide(); + cachedFile.setOptional(isOptional()); + cachedFile.setOnlyOtherSide(!correctSide()); } } } @@ -106,24 +110,24 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { if (failure != null) return; // Ensure it is removed - if (!cachedFile.optionValue || !correctSide()) { - if (cachedFile.cachedLocation == null) return; + if (!cachedFile.getOptionValue() || !correctSide()) { + if (cachedFile.getCachedLocation() == null) return; try { - Files.deleteIfExists(Paths.get(packFolder, cachedFile.cachedLocation)); + Files.deleteIfExists(Paths.get(packFolder, cachedFile.getCachedLocation())); } catch (IOException e) { // TODO: how much of a problem is this? use log4j/other log library to show warning? e.printStackTrace(); } - cachedFile.cachedLocation = null; + cachedFile.setCachedLocation(null); 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 - if (metadata.preserve) { + if (metadata.getPreserve()) { if (destPath.toFile().exists()) { return; } @@ -132,15 +136,17 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { try { Hash hash; String fileHashFormat; - if (metadata.linkedFile != null) { - hash = metadata.linkedFile.getHash(); - fileHashFormat = metadata.linkedFile.download.hashFormat; + ModFile linkedFile = metadata.getLinkedFile(); + if (linkedFile != null) { + hash = linkedFile.getHash(); + fileHashFormat = Objects.requireNonNull(linkedFile.getDownload()).getHashFormat(); } else { - hash = metadata.getHash(); - fileHashFormat = metadata.hashFormat; + hash = metadata.getHashObj(); + fileHashFormat = metadata.getHashFormat(); } Source src = metadata.getSource(indexUri); + assert fileHashFormat != null; GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src); Buffer data = new Buffer(); Okio.buffer(fileSource).readAll(data); @@ -156,9 +162,9 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { 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 - Files.delete(Paths.get(packFolder, cachedFile.cachedLocation)); + Files.delete(Paths.get(packFolder, cachedFile.getCachedLocation())); } } catch (Exception e) { failure = e; @@ -169,16 +175,16 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { } // Update the manifest file try { - cachedFile.hash = metadata.getHash(); + cachedFile.setHash(metadata.getHashObj()); } catch (Exception e) { failure = e; return; } - cachedFile.isOptional = isOptional(); - cachedFile.cachedLocation = metadata.getDestURI().toString(); - if (metadata.linkedFile != null) { + cachedFile.setOptional(isOptional()); + cachedFile.setCachedLocation(metadata.getDestURI().toString()); + if (metadata.getLinkedFile() != null) { try { - cachedFile.linkedFileHash = metadata.linkedFile.getHash(); + cachedFile.setLinkedFileHash(metadata.getLinkedFile().getHash()); } catch (Exception e) { failure = e; } @@ -191,8 +197,8 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { } boolean isOptional() { - if (metadata.linkedFile != null) { - return metadata.linkedFile.isOptional(); + if (metadata.getLinkedFile() != null) { + return metadata.getLinkedFile().isOptional(); } return false; } @@ -202,8 +208,8 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { } boolean correctSide() { - if (metadata.linkedFile != null) { - return metadata.linkedFile.side.hasSide(downloadSide); + if (metadata.getLinkedFile() != null && metadata.getLinkedFile().getSide() != null) { + return metadata.getLinkedFile().getSide().hasSide(downloadSide); } return true; } @@ -214,28 +220,28 @@ class DownloadTask implements IOptionDetails, IExceptionDetails { @Override public boolean getOptionValue() { - return cachedFile.optionValue; + return cachedFile.getOptionValue(); } @Override public String getOptionDescription() { - if (metadata.linkedFile != null) { - return metadata.linkedFile.option.description; + if (metadata.getLinkedFile() != null && metadata.getLinkedFile().getOption() != null) { + return metadata.getLinkedFile().getOption().getDescription(); } return null; } 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 alreadyUpToDate = false; } - cachedFile.optionValue = value; + cachedFile.setOptionValue(value); } static List createTasksFromIndex(IndexFile index, String defaultFormat, UpdateManager.Options.Side downloadSide) { ArrayList tasks = new ArrayList<>(); - for (IndexFile.File file : index.files) { + for (IndexFile.File file : Objects.requireNonNull(index.getFiles())) { tasks.add(new DownloadTask(file, defaultFormat, downloadSide)); } return tasks; diff --git a/src/main/java/link/infra/packwiz/installer/UpdateManager.java b/src/main/java/link/infra/packwiz/installer/UpdateManager.java index 962a15d..7eea84e 100644 --- a/src/main/java/link/infra/packwiz/installer/UpdateManager.java +++ b/src/main/java/link/infra/packwiz/installer/UpdateManager.java @@ -151,17 +151,17 @@ public class UpdateManager { // Invalidation checking must be done here, as it must happen before pack/index hashes are checked List invalidatedUris = new ArrayList<>(); - if (manifest.cachedFiles != null) { - for (Map.Entry entry : manifest.cachedFiles.entrySet()) { + if (manifest.getCachedFiles() != null) { + for (Map.Entry entry : manifest.getCachedFiles().entrySet()) { // ignore onlyOtherSide files - if (entry.getValue().onlyOtherSide) { + if (entry.getValue().getOnlyOtherSide()) { continue; } boolean invalid = false; // if isn't optional, or is optional but optionValue == true - if (!entry.getValue().isOptional || entry.getValue().optionValue) { - if (entry.getValue().cachedLocation != null) { - if (!Paths.get(opts.packFolder, entry.getValue().cachedLocation).toFile().exists()) { + if (!entry.getValue().isOptional() || entry.getValue().getOptionValue()) { + if (entry.getValue().getCachedLocation() != null) { + if (!Paths.get(opts.packFolder, entry.getValue().getCachedLocation()).toFile().exists()) { invalid = true; } } 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!"); // todo: --force? 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()) { showCancellationDialog(); @@ -194,8 +194,8 @@ public class UpdateManager { try { // This is badly written, I'll probably heavily refactor it at some point - processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file), - HashUtils.getHash(pf.index.hashFormat, pf.index.hash), pf.index.hashFormat, manifest, invalidatedUris); + processIndex(HandlerManager.getNewLoc(opts.downloadURI, Objects.requireNonNull(pf.getIndex()).getFile()), + HashUtils.getHash(Objects.requireNonNull(pf.getIndex().getHashFormat()), Objects.requireNonNull(pf.getIndex().getHash())), pf.getIndex().getHashFormat(), manifest, invalidatedUris); } catch (Exception 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 (errorsOccurred) { - manifest.indexFileHash = null; - manifest.packFileHash = null; + manifest.setIndexFileHash(null); + manifest.setPackFileHash(null); } 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())) { gson.toJson(manifest, writer); } catch (IOException e) { @@ -227,13 +227,13 @@ public class UpdateManager { } private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List 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!"); if (!stateHandler.getOptionsButton()) { return; } } - manifest.indexFileHash = indexHash; + manifest.setIndexFileHash(indexHash); GeneralHashingSource indexFileSource; try { @@ -264,33 +264,29 @@ public class UpdateManager { return; } - if (manifest.cachedFiles == null) { - manifest.cachedFiles = new HashMap<>(); - } - ui.submitProgress(new InstallProgress("Checking local files...")); - Iterator> it = manifest.cachedFiles.entrySet().iterator(); + Iterator> it = manifest.getCachedFiles().entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); - if (entry.getValue().cachedLocation != null) { + if (entry.getValue().getCachedLocation() != null) { boolean alreadyDeleted = 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 { - Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation)); + Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation())); } catch (IOException e) { // TODO: should this be shown to the user in some way? e.printStackTrace(); } // Set to null, as it doesn't exist anymore - entry.getValue().cachedLocation = null; + entry.getValue().setCachedLocation(null); 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 if (!alreadyDeleted) { try { - Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation)); + Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation())); } catch (IOException e) { // TODO: should this be shown to the user in some way? e.printStackTrace(); @@ -308,14 +304,13 @@ public class UpdateManager { ui.submitProgress(new InstallProgress("Comparing new files...")); // TODO: progress bar? - if (indexFile.files == null || indexFile.files.size() == 0) { + if (indexFile.getFiles().isEmpty()) { System.out.println("Warning: Index is empty!"); - indexFile.files = new ArrayList<>(); } - List tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.hashFormat, opts.side); + List tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.getHashFormat(), opts.side); // If the side changes, invalidate EVERYTHING just in case // 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) { 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? if (invalidateAll) { f.invalidate(); - } else if (invalidatedUris.contains(f.metadata.file)) { + } else if (invalidatedUris.contains(f.metadata.getFile())) { f.invalidate(); } - ManifestFile.File file = manifest.cachedFiles.get(f.metadata.file); + ManifestFile.File file = manifest.getCachedFiles().get(f.metadata.getFile()); 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 file.backup(); @@ -412,11 +407,11 @@ public class UpdateManager { if (task.getException() != null) { ManifestFile.File file = task.cachedFile.getRevert(); if (file != null) { - manifest.cachedFiles.putIfAbsent(task.metadata.file, file); + manifest.getCachedFiles().putIfAbsent(task.metadata.getFile(), file); } } else { // 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); } } diff --git a/src/main/java/link/infra/packwiz/installer/metadata/EfficientBooleanAdapter.java b/src/main/java/link/infra/packwiz/installer/metadata/EfficientBooleanAdapter.java deleted file mode 100644 index 28f2f3f..0000000 --- a/src/main/java/link/infra/packwiz/installer/metadata/EfficientBooleanAdapter.java +++ /dev/null @@ -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 { - - @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(); - } -} diff --git a/src/main/java/link/infra/packwiz/installer/metadata/IndexFile.java b/src/main/java/link/infra/packwiz/installer/metadata/IndexFile.java deleted file mode 100644 index 0a7e50d..0000000 --- a/src/main/java/link/infra/packwiz/installer/metadata/IndexFile.java +++ /dev/null @@ -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 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; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/link/infra/packwiz/installer/metadata/ManifestFile.java b/src/main/java/link/infra/packwiz/installer/metadata/ManifestFile.java deleted file mode 100644 index b39cfe4..0000000 --- a/src/main/java/link/infra/packwiz/installer/metadata/ManifestFile.java +++ /dev/null @@ -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 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; - } - } -} \ No newline at end of file diff --git a/src/main/java/link/infra/packwiz/installer/metadata/ModFile.java b/src/main/java/link/infra/packwiz/installer/metadata/ModFile.java deleted file mode 100644 index ea2754d..0000000 --- a/src/main/java/link/infra/packwiz/installer/metadata/ModFile.java +++ /dev/null @@ -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 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; - } - -} \ No newline at end of file diff --git a/src/main/java/link/infra/packwiz/installer/metadata/PackFile.java b/src/main/java/link/infra/packwiz/installer/metadata/PackFile.java deleted file mode 100644 index a9e7617..0000000 --- a/src/main/java/link/infra/packwiz/installer/metadata/PackFile.java +++ /dev/null @@ -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 versions; - public Map client; - public Map server; -} \ No newline at end of file diff --git a/src/main/java/link/infra/packwiz/installer/metadata/SpaceSafeURI.java b/src/main/java/link/infra/packwiz/installer/metadata/SpaceSafeURI.java deleted file mode 100644 index 00c83a5..0000000 --- a/src/main/java/link/infra/packwiz/installer/metadata/SpaceSafeURI.java +++ /dev/null @@ -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, 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(); - } -} diff --git a/src/main/java/link/infra/packwiz/installer/metadata/SpaceSafeURIParser.java b/src/main/java/link/infra/packwiz/installer/metadata/SpaceSafeURIParser.java deleted file mode 100644 index 9a91638..0000000 --- a/src/main/java/link/infra/packwiz/installer/metadata/SpaceSafeURIParser.java +++ /dev/null @@ -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 { - - @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? - -} \ No newline at end of file diff --git a/src/main/java/link/infra/packwiz/installer/request/HandlerManager.java b/src/main/java/link/infra/packwiz/installer/request/HandlerManager.java deleted file mode 100644 index 7afdb77..0000000 --- a/src/main/java/link/infra/packwiz/installer/request/HandlerManager.java +++ /dev/null @@ -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 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 -} diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/EfficientBooleanAdapter.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/EfficientBooleanAdapter.kt new file mode 100644 index 0000000..a0bba6d --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/EfficientBooleanAdapter.kt @@ -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() { + @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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/IndexFile.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/IndexFile.kt new file mode 100644 index 0000000..24ba614 --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/IndexFile.kt @@ -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 = 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 + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/ManifestFile.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/ManifestFile.kt new file mode 100644 index 0000000..180d74f --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/ManifestFile.kt @@ -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 = 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 + } + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/ModFile.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/ModFile.kt new file mode 100644 index 0000000..b84271d --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/ModFile.kt @@ -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? = 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 +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/PackFile.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/PackFile.kt new file mode 100644 index 0000000..92e32b1 --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/PackFile.kt @@ -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? = null + var client: Map? = null + var server: Map? = null +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/SpaceSafeURI.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/SpaceSafeURI.kt new file mode 100644 index 0000000..664c15d --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/SpaceSafeURI.kt @@ -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, 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() +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/metadata/SpaceSafeURIParser.kt b/src/main/kotlin/link/infra/packwiz/installer/metadata/SpaceSafeURIParser.kt new file mode 100644 index 0000000..092d68b --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/metadata/SpaceSafeURIParser.kt @@ -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 { + @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? +} \ No newline at end of file diff --git a/src/main/kotlin/link/infra/packwiz/installer/request/HandlerManager.kt b/src/main/kotlin/link/infra/packwiz/installer/request/HandlerManager.kt new file mode 100644 index 0000000..43f338e --- /dev/null +++ b/src/main/kotlin/link/infra/packwiz/installer/request/HandlerManager.kt @@ -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 = 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 +} \ No newline at end of file