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
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ public Collection<JavaRuntime> getAllJava(Platform platform) {
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
Path tempDir = getPlatformRoot(platform).resolve(".tmp").resolve(javaDir.getFileName());

return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
return new MojangJavaDownloadTask(downloadProvider, javaDir, tempDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
Expand All @@ -167,14 +168,14 @@ public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider,
if (JavaManager.isCompatible(platform))
info = JavaInfoUtils.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
info = new JavaInfo(platform, result.download().version().name(), null);

Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.component());

Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
result.remoteFiles().getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,40 +37,45 @@
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.*;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;

public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Result> {

private static final String JAVA_LIST_URL = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";

private final DownloadProvider downloadProvider;
private final Path target;
private final Path tempDir;
private final Task<MojangJavaRemoteFiles> javaDownloadsTask;
private final List<Task<?>> dependencies = new ArrayList<>();

private volatile MojangJavaDownloads.JavaDownload download;

public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, GameJavaVersion javaVersion, String platform) {
public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, Path tempDir, GameJavaVersion javaVersion, String platform) {
this.target = target;
this.tempDir = tempDir;
this.downloadProvider = downloadProvider;
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"))
.thenComposeAsync(javaDownloadsJson -> {
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);

Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
if (osDownloads == null || !osDownloads.containsKey(javaVersion.component()))
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.component());
for (MojangJavaDownloads.JavaDownload download : candidates) {
if (JavaInfo.parseVersion(download.getVersion().getName()) >= javaVersion.majorVersion()) {
this.download = download;
return new GetTask(downloadProvider.injectURLWithCandidates(download.getManifest().getUrl()));
}
}
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
})
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(JAVA_LIST_URL))
.thenComposeAsync(javaDownloadsJson -> {
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);

Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.downloads().get(platform);
if (osDownloads == null || !osDownloads.containsKey(javaVersion.component()))
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.component());
for (MojangJavaDownloads.JavaDownload download : candidates) {
if (JavaInfo.parseVersion(download.version().name()) >= javaVersion.majorVersion()) {
this.download = download;
return new GetTask(downloadProvider.injectURLWithCandidates(download.manifest().getUrl()));
}
}
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
})
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
}

@Override
Expand All @@ -81,10 +86,8 @@ public Collection<Task<?>> getDependents() {
@Override
public void execute() throws Exception {
for (Map.Entry<String, MojangJavaRemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
Path dest = target.resolve(entry.getKey());
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile) {
MojangJavaRemoteFiles.RemoteFile file = ((MojangJavaRemoteFiles.RemoteFile) entry.getValue());

Path dest = tempDir.resolve(entry.getKey());
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile file) {
// Use local file if it already exists
try {
BasicFileAttributes localFileAttributes = Files.readAttributes(dest, BasicFileAttributes.class);
Expand All @@ -101,17 +104,34 @@ public void execute() throws Exception {

if (file.getDownloads().containsKey("lzma")) {
DownloadInfo download = file.getDownloads().get("lzma");
Path tempFile = target.resolve(entry.getKey() + ".lzma");
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
DownloadInfo raw = file.getDownloads().get("raw");

String rawSha1;
if (raw != null && raw.getSha1() != null) {
rawSha1 = raw.getSha1();
} else {
rawSha1 = null;
}

Path tempFile = tempDir.resolve(entry.getKey() + ".lzma");
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile,
new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
task.setName(entry.getKey());
dependencies.add(task.thenRunAsync(() -> {
Path decompressed = target.resolve(entry.getKey() + ".tmp");
try (LZMAInputStream input = new LZMAInputStream(Files.newInputStream(tempFile))) {
Path decompressed = tempDir.resolve(entry.getKey() + ".tmp");
var digest = MessageDigest.getInstance("SHA-1");
try (var input = new DigestInputStream(new LZMAInputStream(Files.newInputStream(tempFile)), digest)) {
Files.copy(input, decompressed, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new ArtifactMalformedException("File " + entry.getKey() + " is malformed", e);
}

String actualSha1 = HexFormat.of().formatHex(digest.digest());

if (rawSha1 != null && !actualSha1.equalsIgnoreCase(rawSha1)) {
throw new ArtifactMalformedException("File " + entry.getKey() + " has incorrect SHA-1 hash: expected " + rawSha1 + ", got " + actualSha1);
}

try {
Files.deleteIfExists(tempFile);
} catch (IOException e) {
Expand All @@ -137,8 +157,7 @@ public void execute() throws Exception {
}
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteDirectory) {
Files.createDirectories(dest);
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink) {
MojangJavaRemoteFiles.RemoteLink link = ((MojangJavaRemoteFiles.RemoteLink) entry.getValue());
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink link) {
Files.deleteIfExists(dest);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

Creating RemoteLink entries doesn’t ensure the parent directory exists. Since javaDownloadsTask.getResult().getFiles() is a Map with no guaranteed iteration order, a link may be processed before its parent RemoteDirectory, causing Files.createSymbolicLink(dest, ...) to fail with NoSuchFileException. Create dest.getParent() directories before creating the symlink (when parent is non-null).

Suggested change
Files.deleteIfExists(dest);
Files.deleteIfExists(dest);
Path parent = dest.getParent();
if (parent != null) {
Files.createDirectories(parent);
}

Copilot uses AI. Check for mistakes.
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
}
Expand All @@ -157,16 +176,19 @@ public boolean doPostExecute() {

@Override
public void postExecute() throws Exception {
setResult(new Result(download, javaDownloadsTask.getResult()));
if (isDependenciesSucceeded()) {
FileUtils.cleanDirectory(target);

if (Files.getFileStore(target).equals(Files.getFileStore(tempDir))) {
Files.move(tempDir, target, StandardCopyOption.REPLACE_EXISTING);
} else {
FileUtils.copyDirectory(tempDir, target);
FileUtils.deleteDirectory(tempDir);
}
setResult(new Result(download, javaDownloadsTask.getResult()));
}
}

public static final class Result {
public final MojangJavaDownloads.JavaDownload download;
public final MojangJavaRemoteFiles remoteFiles;

public Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) {
this.download = download;
this.remoteFiles = remoteFiles;
}
public record Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.JsonSerializable;

import java.lang.reflect.Type;
import java.util.List;
Expand All @@ -29,21 +29,10 @@
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;

@JsonSerializable
@JsonAdapter(MojangJavaDownloads.Adapter.class)
public class MojangJavaDownloads {

private final Map<String, Map<String, List<JavaDownload>>> downloads;

public MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
this.downloads = downloads;
}

public Map<String, Map<String, List<JavaDownload>>> getDownloads() {
return downloads;
}

public record MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
public static class Adapter implements JsonSerializer<MojangJavaDownloads>, JsonDeserializer<MojangJavaDownloads> {

@Override
public JsonElement serialize(MojangJavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.downloads);
Expand All @@ -55,78 +44,15 @@ public MojangJavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeser
}
}

@Immutable
public static class JavaDownload {
private final Availability availability;
private final DownloadInfo manifest;
private final Version version;

public JavaDownload() {
this(new Availability(), new DownloadInfo(), new Version());
}

public JavaDownload(Availability availability, DownloadInfo manifest, Version version) {
this.availability = availability;
this.manifest = manifest;
this.version = version;
}

public Availability getAvailability() {
return availability;
}

public DownloadInfo getManifest() {
return manifest;
}

public Version getVersion() {
return version;
}
@JsonSerializable
public record JavaDownload(Availability availability, DownloadInfo manifest, Version version) {
}

@Immutable
public static class Availability {
private final int group;
private final int progress;

public Availability() {
this(0, 0);
}

public Availability(int group, int progress) {
this.group = group;
this.progress = progress;
}

public int getGroup() {
return group;
}

public int getProgress() {
return progress;
}
@JsonSerializable
public record Availability(int group, int progress) {
}

@Immutable
public static class Version {
private final String name;
private final String released;

public Version() {
this("", "");
}

public Version(String name, String released) {
this.name = name;
this.released = released;
}

public String getName() {
return name;
}

public String getReleased() {
return released;
}
@JsonSerializable
public record Version(String name, String released) {
}
}