Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 62 additions & 32 deletions src/main/java/com/vinurl/exe/Executable.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -22,6 +25,7 @@
import java.util.zip.ZipInputStream;

import static com.vinurl.client.VinURLClient.CONFIG;
import static com.vinurl.exe.GitHub.ReleaseInfo;
import static com.vinurl.util.Constants.LOGGER;
import static com.vinurl.util.Constants.VINURLPATH;

Expand Down Expand Up @@ -92,7 +96,8 @@ public void killAllProcesses() {
public boolean checkForExecutable() {
if (DIRECTORY.toFile().exists() || DIRECTORY.toFile().mkdirs()) {
if (!FILE_PATH.toFile().exists()) {
return downloadExecutable();
ReleaseInfo release = GitHub.fetchLatestRelease(REPOSITORY_NAME, REPOSITORY_FILE);
return !release.isEmpty() && downloadExecutable(release);
} else if (CONFIG.updatesOnStartup()) {
checkForUpdates();
}
Expand All @@ -102,34 +107,73 @@ public boolean checkForExecutable() {
}

public boolean checkForUpdates() {
return !currentVersion().equals(latestVersion()) && downloadExecutable();
ReleaseInfo release = GitHub.fetchLatestRelease(REPOSITORY_NAME, REPOSITORY_FILE);
if (release.isEmpty() || release.version().equals(currentVersion())) {
return false;
}
return downloadExecutable(release);
}

private boolean downloadExecutable() {
try (InputStream inputStream = getDownloadInputStream()) {
private boolean downloadExecutable(ReleaseInfo release) {
Path tempFile = null;
try {
tempFile = Files.createTempFile(DIRECTORY, FILE_NAME, ".tmp");
tempFile.toFile().deleteOnExit();

String actualDigest = downloadToFile(GitHub.openAssetStream(REPOSITORY_NAME, REPOSITORY_FILE), tempFile);

if (release.digest() != null && !release.digest().equals(actualDigest)) {
LOGGER.error("Digest verification failed for {} (expected {}, got {})",
REPOSITORY_FILE, release.digest(), actualDigest);
return false;
}

if (REPOSITORY_FILE.endsWith(".zip")) {
try (ZipInputStream zipInput = new ZipInputStream(inputStream)) {
ZipEntry zipEntry = zipInput.getNextEntry();
while (zipEntry != null) {
if (zipEntry.getName().endsWith(FILE_NAME + (SystemUtils.IS_OS_WINDOWS ? ".exe" : ""))) {
Files.copy(zipInput, FILE_PATH, StandardCopyOption.REPLACE_EXISTING);
break;
}
zipEntry = zipInput.getNextEntry();
}
}
extractFromZip(tempFile, FILE_PATH);
} else {
Files.copy(inputStream, FILE_PATH, StandardCopyOption.REPLACE_EXISTING);
Files.move(tempFile, FILE_PATH, StandardCopyOption.REPLACE_EXISTING);
tempFile = null;
}

if (SystemUtils.IS_OS_UNIX) {
Runtime.getRuntime().exec(new String[] {"chmod", "+x", FILE_PATH.toString()});
}
return createVersionFile(latestVersion());
return createVersionFile(release.version());
} catch (Exception e) {
LOGGER.error("Failed to download {}", REPOSITORY_FILE, e);
return false;
} finally {
if (tempFile != null) {
try {
Files.deleteIfExists(tempFile);
} catch (IOException ignored) {}
}
}
}

private String downloadToFile(InputStream input, Path target) throws IOException, NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (DigestInputStream digestInput = new DigestInputStream(input, md);
OutputStream output = Files.newOutputStream(target)) {
digestInput.transferTo(output);
}
return "sha256:" + HexFormat.of().formatHex(md.digest());
}

private void extractFromZip(Path zipFile, Path target) throws IOException {
String targetName = FILE_NAME + (SystemUtils.IS_OS_WINDOWS ? ".exe" : "");
try (ZipInputStream zipInput = new ZipInputStream(Files.newInputStream(zipFile))) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().endsWith(targetName)) {
Files.copy(zipInput, target, StandardCopyOption.REPLACE_EXISTING);
return;
}
}
}
throw new IOException("Entry not found in zip: " + targetName);
}

private boolean createVersionFile(String version) {
try {
Files.writeString(VERSION_PATH, version);
Expand All @@ -147,20 +191,6 @@ public String currentVersion() {
}
}

private String latestVersion() {
String url = "https://api.github.com/repos/%s/releases/latest".formatted(REPOSITORY_NAME);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new URI(url).toURL().openStream()))) {
return reader.readLine().split("\"tag_name\":\"")[1].split("\",\"target_commitish\"")[0];
} catch (IOException | ArrayIndexOutOfBoundsException | URISyntaxException e) {
return "";
}
}

private InputStream getDownloadInputStream() throws IOException, URISyntaxException {
String url = "https://github.com/%s/releases/latest/download/%s".formatted(REPOSITORY_NAME, REPOSITORY_FILE);
return new URI(url).toURL().openStream();
}

public ProcessStream executeCommand(String id, String... arguments) {
return new ProcessStream(id, arguments);
}
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/com/vinurl/exe/GitHub.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.vinurl.exe;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.minecraft.client.Minecraft;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;

import static com.vinurl.util.Constants.MOD_ID;
import static com.vinurl.util.Constants.MOD_VERSION;

public class GitHub {
private static final String USER_AGENT = "Java/%s %s/%s".formatted(System.getProperty("java.version"), MOD_ID, MOD_VERSION);
private static final String API_VERSION = "2022-11-28";

public record ReleaseInfo(String version, @Nullable String digest) {
public static final ReleaseInfo EMPTY = new ReleaseInfo("", null);

public boolean isEmpty() {
return version.isEmpty();
}
}

public static ReleaseInfo fetchLatestRelease(String repository, String assetName) {
String url = "https://api.github.com/repos/%s/releases/latest".formatted(repository);
try (InputStream stream = openApiStream(url);
InputStreamReader reader = new InputStreamReader(stream)) {
JsonObject release = JsonParser.parseReader(reader).getAsJsonObject();

String version = release.get("tag_name").getAsString();

String digest = null;
JsonArray assets = release.getAsJsonArray("assets");
for (JsonElement asset : assets) {
JsonObject assetObj = asset.getAsJsonObject();
if (assetName.equals(assetObj.get("name").getAsString())) {
JsonElement digestElement = assetObj.get("digest");
if (digestElement != null && !digestElement.isJsonNull()) {
digest = digestElement.getAsString();
}
break;
}
}

return new ReleaseInfo(version, digest);
} catch (Exception e) {
return ReleaseInfo.EMPTY;
}
}

public static InputStream openAssetStream(String repository, String assetName) throws IOException {
String url = "https://github.com/%s/releases/latest/download/%s".formatted(repository, assetName);
return openStream(url);
}

private static InputStream openApiStream(String url) throws IOException {
HttpURLConnection conn = openConnection(url);
conn.setRequestProperty("Accept", "application/vnd.github+json");
conn.setRequestProperty("X-GitHub-Api-Version", API_VERSION);
return conn.getInputStream();
}

private static InputStream openStream(String url) throws IOException {
return openConnection(url).getInputStream();
}

private static HttpURLConnection openConnection(String url) throws IOException {
try {
HttpURLConnection conn = (HttpURLConnection) new URI(url).toURL().openConnection(Minecraft.getInstance().getProxy());
conn.setRequestProperty("User-Agent", USER_AGENT);
return conn;
} catch (URISyntaxException e) {
throw new IOException("Invalid URL: " + url, e);
}
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/vinurl/util/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
public class Constants {
//general
public static final String MOD_ID = "vinurl";
public static final String MOD_VERSION = FabricLoader.getInstance().getModContainer(MOD_ID)
.map(c -> c.getMetadata().getVersion().getFriendlyString()).orElse("unknown");
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public static final Path VINURLPATH = FabricLoader.getInstance().getGameDir().resolve(MOD_ID);

Expand Down