统一转义 URI 中的特殊字符 (#4181)

This commit is contained in:
Glavo
2025-08-03 19:34:34 +08:00
committed by GitHub
parent e2df8e25b1
commit 74c647cc97
44 changed files with 259 additions and 224 deletions

View File

@@ -267,7 +267,7 @@ public final class LauncherHelper {
if (ex.getCause() instanceof ResponseCodeException) { if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause(); ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode(); int responseCode = rce.getResponseCode();
URI uri = rce.getUri(); String uri = rce.getUri();
if (responseCode == 404) if (responseCode == 404)
message += i18n("download.code.404", uri); message += i18n("download.code.404", uri);
else else
@@ -298,7 +298,7 @@ public final class LauncherHelper {
} else if (ex instanceof ResponseCodeException) { } else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex; ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode(); int responseCode = rce.getResponseCode();
URI uri = rce.getUri(); String uri = rce.getUri();
if (responseCode == 404) if (responseCode == 404)
message = i18n("download.code.404", uri); message = i18n("download.code.404", uri);
else else

View File

@@ -45,7 +45,6 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -110,7 +109,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) { if (!Files.isRegularFile(file)) {
// download it // download it
try { try {
new FileDownloadTask(URI.create(texture.getUrl()), file).run(); new FileDownloadTask(texture.getUrl(), file).run();
LOG.info("Texture downloaded: " + texture.getUrl()); LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) { } catch (Exception e) {
if (Files.isRegularFile(file)) { if (Files.isRegularFile(file)) {

View File

@@ -836,7 +836,8 @@ public final class FXUtils {
} }
} }
public static Image loadImage(URI uri) throws Exception { public static Image loadImage(String url) throws Exception {
URI uri = NetworkUtils.toURI(url);
URLConnection connection = NetworkUtils.createConnection(uri); URLConnection connection = NetworkUtils.createConnection(uri);
if (connection instanceof HttpURLConnection) { if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection); connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
@@ -896,7 +897,7 @@ public final class FXUtils {
} }
public static Task<Image> getRemoteImageTask(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) { public static Task<Image> getRemoteImageTask(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(URI.create(url)) return new CacheFileTask(url)
.thenApplyAsync(file -> { .thenApplyAsync(file -> {
try (var channel = FileChannel.open(file, StandardOpenOption.READ)) { try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {
var header = new byte[12]; var header = new byte[12];

View File

@@ -22,6 +22,7 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.util.StringUtils;
import org.jsoup.nodes.Node; import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode; import org.jsoup.nodes.TextNode;
@@ -159,18 +160,9 @@ public final class HTMLRenderer {
private void appendImage(Node node) { private void appendImage(Node node) {
String src = node.absUrl("src"); String src = node.absUrl("src");
URI imageUri = null;
try {
if (!src.isEmpty())
imageUri = URI.create(src);
} catch (Exception ignored) {
}
String alt = node.attr("alt"); String alt = node.attr("alt");
if (imageUri != null) { if (StringUtils.isNotBlank(src)) {
URI uri = URI.create(src);
String widthAttr = node.attr("width"); String widthAttr = node.attr("width");
String heightAttr = node.attr("height"); String heightAttr = node.attr("height");
@@ -191,7 +183,7 @@ public final class HTMLRenderer {
} }
try { try {
Image image = FXUtils.getRemoteImageTask(uri.toString(), width, height, true, true) Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
.run(); .run();
if (image == null) if (image == null)
throw new AssertionError("Image loading task returned null"); throw new AssertionError("Image loading task returned null");
@@ -207,7 +199,7 @@ public final class HTMLRenderer {
children.add(imageView); children.add(imageView);
return; return;
} catch (Throwable e) { } catch (Throwable e) {
LOG.warning("Failed to load image: " + uri, e); LOG.warning("Failed to load image: " + src, e);
} }
} }

View File

@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ClassTitle; import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList; import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jackhuang.hmcl.util.platform.NativeUtils; import org.jackhuang.hmcl.util.platform.NativeUtils;
@@ -48,7 +49,6 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.windows.Kernel32; import org.jackhuang.hmcl.util.platform.windows.Kernel32;
import org.jackhuang.hmcl.util.platform.windows.WinConstants; import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import java.net.URI;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
@@ -190,7 +190,7 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
item.titleProperty().bind(title); item.titleProperty().bind(title);
String host = ""; String host = "";
try { try {
host = URI.create(server.getUrl()).getHost(); host = NetworkUtils.toURI(server.getUrl()).getHost();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e); LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
} }

View File

@@ -34,7 +34,6 @@ import org.jackhuang.hmcl.util.Lang;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -196,7 +195,8 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
lblServerName.setText(serverBeingAdded.getName()); lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl()); lblServerUrl.setText(serverBeingAdded.getUrl());
lblServerWarning.setVisible("http".equals(URI.create(serverBeingAdded.getUrl()).getScheme())); //noinspection HttpUrlsUsage
lblServerWarning.setVisible(serverBeingAdded.getUrl().startsWith("http://"));
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT); this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
} else { } else {

View File

@@ -21,10 +21,7 @@ import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl; import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -58,9 +55,9 @@ public class URLValidator extends ValidatorBase {
hasErrors.set(!nullable); hasErrors.set(!nullable);
else { else {
try { try {
new URL(textField.getText()).toURI(); NetworkUtils.toURI(textField.getText());
hasErrors.set(false); hasErrors.set(false);
} catch (IOException | URISyntaxException e) { } catch (IllegalArgumentException e) {
hasErrors.set(true); hasErrors.set(true);
} }
} }

View File

@@ -58,7 +58,6 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -214,7 +213,7 @@ public class DecoratorController {
String backgroundImageUrl = config().getBackgroundImageUrl(); String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null) { if (backgroundImageUrl != null) {
try { try {
image = FXUtils.loadImage(URI.create(backgroundImageUrl)); image = FXUtils.loadImage(backgroundImageUrl);
} catch (Exception e) { } catch (Exception e) {
LOG.warning("Couldn't load background image", e); LOG.warning("Couldn't load background image", e);
} }

View File

@@ -55,7 +55,6 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
@@ -156,7 +155,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path dest = runDirectory.resolve(subdirectoryName).resolve(result); Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
Controllers.taskDialog(Task.composeAsync(() -> { Controllers.taskDialog(Task.composeAsync(() -> {
var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest); var task = new FileDownloadTask(file.getFile().getUrl(), dest);
task.setName(file.getName()); task.setName(file.getName());
return task; return task;
}).whenComplete(Schedulers.javafx(), exception -> { }).whenComplete(Schedulers.javafx(), exception -> {

View File

@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map; import java.util.Map;
@@ -124,10 +123,9 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
} }
private void onChooseRemoteFile() { private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> { Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (url, resolve, reject) -> {
try { try {
URI url = URI.create(urlString); if (url.endsWith("server-manifest.json")) {
if (urlString.endsWith("server-manifest.json")) {
// if urlString ends with .json, we assume that the url is server-manifest.json // if urlString ends with .json, we assume that the url is server-manifest.json
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> { Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class); ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class);
@@ -148,7 +146,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
resolve.run(); resolve.run();
Controllers.taskDialog( Controllers.taskDialog(
new FileDownloadTask(url, modpack, null) new FileDownloadTask(url, modpack)
.whenComplete(Schedulers.javafx(), e -> { .whenComplete(Schedulers.javafx(), e -> {
if (e == null) { if (e == null) {
resolve.run(); resolve.run();

View File

@@ -133,7 +133,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
if (exception.getCause() instanceof ResponseCodeException) { if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) exception.getCause(); ResponseCodeException rce = (ResponseCodeException) exception.getCause();
int responseCode = rce.getResponseCode(); int responseCode = rce.getResponseCode();
URI uri = rce.getUri(); String uri = rce.getUri();
if (responseCode == 404) if (responseCode == 404)
message += i18n("download.code.404", uri); message += i18n("download.code.404", uri);
else else

View File

@@ -54,7 +54,6 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.net.URI;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -180,7 +179,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Controllers.taskDialog( Controllers.taskDialog(
Task.composeAsync(() -> { Task.composeAsync(() -> {
var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest.toPath(), file.getFile().getIntegrityCheck()); var task = new FileDownloadTask(file.getFile().getUrl(), dest.toPath(), file.getFile().getIntegrityCheck());
task.setName(file.getName()); task.setName(file.getName());
return task; return task;
}), }),

View File

@@ -45,7 +45,6 @@ import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CSVTable; import org.jackhuang.hmcl.util.io.CSVTable;
import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -294,7 +293,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
fileName += ModManager.DISABLED_EXTENSION; fileName += ModManager.DISABLED_EXTENSION;
var task = new FileDownloadTask( var task = new FileDownloadTask(
URI.create(remote.getFile().getUrl()), remote.getFile().getUrl(),
modManager.getModsDirectory().resolve(fileName)); modManager.getModsDirectory().resolve(fileName));
task.setName(remote.getName()); task.setName(remote.getName());

View File

@@ -32,9 +32,6 @@ import javafx.scene.control.Toggle;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ProcessPriority;
import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.setting.*;

View File

@@ -40,12 +40,12 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider; import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
@@ -75,9 +75,9 @@ public final class Versions {
Path modpack; Path modpack;
URI downloadURL; URI downloadURL;
try { try {
downloadURL = NetworkUtils.toURI(file.getFile().getUrl());
modpack = Files.createTempFile("modpack", ".zip"); modpack = Files.createTempFile("modpack", ".zip");
downloadURL = new URI(file.getFile().getUrl()); } catch (IOException | IllegalArgumentException e) {
} catch (IOException | URISyntaxException e) {
Controllers.dialog( Controllers.dialog(
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e), i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR); i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);

View File

@@ -23,7 +23,6 @@ import org.tukaani.xz.XZInputStream;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.jar.JarOutputStream; import java.util.jar.JarOutputStream;
@@ -33,7 +32,7 @@ final class HMCLDownloadTask extends FileDownloadTask {
private final RemoteVersion.Type archiveFormat; private final RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) { public HMCLDownloadTask(RemoteVersion version, Path target) {
super(URI.create(version.getUrl()), target, version.getIntegrityCheck()); super(version.getUrl(), target, version.getIntegrityCheck());
archiveFormat = version.getType(); archiveFormat = version.getType();
} }

View File

@@ -26,14 +26,13 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.Optional; import java.util.Optional;
public class RemoteVersion { public class RemoteVersion {
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException { public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
try { try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(URI.create(url)), JsonObject.class); JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(url), JsonObject.class);
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing")); String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null); String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null); String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);

View File

@@ -29,6 +29,7 @@ import java.net.URISyntaxException;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.regex.Pattern;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.HttpRequest;
@@ -58,10 +59,10 @@ public class AuthlibInjectorServer implements Observable {
public static AuthlibInjectorServer locateServer(String url) throws IOException { public static AuthlibInjectorServer locateServer(String url) throws IOException {
try { try {
url = addHttpsIfMissing(url); url = addHttpsIfMissing(url);
HttpURLConnection conn = NetworkUtils.createHttpConnection(URI.create(url)); HttpURLConnection conn = NetworkUtils.createHttpConnection(url);
String ali = conn.getHeaderField("x-authlib-injector-api-location"); String ali = conn.getHeaderField("x-authlib-injector-api-location");
if (ali != null) { if (ali != null) {
URI absoluteAli = conn.getURL().toURI().resolve(ali); URI absoluteAli = conn.getURL().toURI().resolve(NetworkUtils.toURI(ali));
if (!urlEqualsIgnoreSlash(url, absoluteAli.toString())) { if (!urlEqualsIgnoreSlash(url, absoluteAli.toString())) {
conn.disconnect(); conn.disconnect();
url = absoluteAli.toString(); url = absoluteAli.toString();
@@ -85,15 +86,15 @@ public class AuthlibInjectorServer implements Observable {
} }
private static String addHttpsIfMissing(String url) throws IOException { private static String addHttpsIfMissing(String url) throws IOException {
URI uri = URI.create(url); if (Pattern.compile("^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*)://").matcher(url).find())
if (uri.getScheme() == null) {
return "https://" + url;
} else if (!NetworkUtils.isHttpUri(uri)) {
throw new IOException("Yggdrasil server should be an HTTP or HTTPS URI, but got: " + url);
} else {
return url; return url;
}
if (url.startsWith("//"))
return "https:" + url;
else if (url.startsWith("/"))
return "https:/" + url;
else
return "https://" + url;
} }
private static boolean urlEqualsIgnoreSlash(String a, String b) { private static boolean urlEqualsIgnoreSlash(String a, String b) {

View File

@@ -34,7 +34,6 @@ import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -165,7 +164,7 @@ public class MicrosoftService {
.accept("application/json").createConnection(); .accept("application/json").createConnection();
if (request.getResponseCode() != 200) { if (request.getResponseCode() != 200) {
throw new ResponseCodeException(URI.create("https://api.minecraftservices.com/entitlements/mcstore"), request.getResponseCode()); throw new ResponseCodeException("https://api.minecraftservices.com/entitlements/mcstore", request.getResponseCode());
} }
// Get Minecraft Account UUID // Get Minecraft Account UUID
@@ -248,7 +247,7 @@ public class MicrosoftService {
if (responseCode == HTTP_NOT_FOUND) { if (responseCode == HTTP_NOT_FOUND) {
throw new NoMinecraftJavaEditionProfileException(); throw new NoMinecraftJavaEditionProfileException();
} else if (responseCode != 200) { } else if (responseCode != 200) {
throw new ResponseCodeException(URI.create("https://api.minecraftservices.com/minecraft/profile"), responseCode); throw new ResponseCodeException("https://api.minecraftservices.com/minecraft/profile", responseCode);
} }
String result = NetworkUtils.readFullyAsString(conn); String result = NetworkUtils.readFullyAsString(conn);
@@ -258,12 +257,12 @@ public class MicrosoftService {
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException { public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
Objects.requireNonNull(uuid); Objects.requireNonNull(uuid);
return Optional.ofNullable(GSON.fromJson(request(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)), null), CompleteGameProfile.class)); return Optional.ofNullable(GSON.fromJson(request("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid), null), CompleteGameProfile.class));
} }
public void uploadSkin(String accessToken, boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException { public void uploadSkin(String accessToken, boolean isSlim, Path file) throws AuthenticationException, UnsupportedOperationException {
try { try {
HttpURLConnection con = NetworkUtils.createHttpConnection(URI.create("https://api.minecraftservices.com/minecraft/profile/skins")); HttpURLConnection con = NetworkUtils.createHttpConnection("https://api.minecraftservices.com/minecraft/profile/skins");
con.setRequestMethod("POST"); con.setRequestMethod("POST");
con.setRequestProperty("Authorization", "Bearer " + accessToken); con.setRequestProperty("Authorization", "Bearer " + accessToken);
con.setDoOutput(true); con.setDoOutput(true);
@@ -288,12 +287,12 @@ public class MicrosoftService {
} }
} }
private static String request(URI url, Object payload) throws AuthenticationException { private static String request(String url, Object payload) throws AuthenticationException {
try { try {
if (payload == null) if (payload == null)
return NetworkUtils.doGet(url); return NetworkUtils.doGet(url);
else else
return NetworkUtils.doPost(url, payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json"); return NetworkUtils.doPost(NetworkUtils.toURI(url), payload instanceof String ? (String) payload : GSON.toJson(payload), "application/json");
} catch (IOException e) { } catch (IOException e) {
throw new ServerDisconnectException(e); throw new ServerDisconnectException(e);
} }

View File

@@ -27,13 +27,13 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -166,7 +166,7 @@ public class Skin {
String realCslApi = type == Type.LITTLE_SKIN String realCslApi = type == Type.LITTLE_SKIN
? "https://littleskin.cn/csl" ? "https://littleskin.cn/csl"
: StringUtils.removeSuffix(Lang.requireNonNullElse(cslApi, ""), "/"); : StringUtils.removeSuffix(Lang.requireNonNullElse(cslApi, ""), "/");
return Task.composeAsync(() -> new GetTask(URI.create(String.format("%s/%s.json", realCslApi, username)))) return Task.composeAsync(() -> new GetTask(String.format("%s/%s.json", realCslApi, username)))
.thenComposeAsync(json -> { .thenComposeAsync(json -> {
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class); SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
@@ -176,8 +176,8 @@ public class Skin {
return Task.allOf( return Task.allOf(
Task.supplyAsync(result::getModel), Task.supplyAsync(result::getModel),
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(URI.create(String.format("%s/textures/%s", realCslApi, result.getHash())), 3), result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(String.format("%s/textures/%s", realCslApi, result.getHash())),
result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(URI.create(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3) result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(String.format("%s/textures/%s", realCslApi, result.getCapeHash()))
); );
}).thenApplyAsync(result -> { }).thenApplyAsync(result -> {
if (result == null) { if (result == null) {
@@ -229,8 +229,8 @@ public class Skin {
private static class FetchBytesTask extends FetchTask<InputStream> { private static class FetchBytesTask extends FetchTask<InputStream> {
public FetchBytesTask(URI uri, int retry) { public FetchBytesTask(String uri) {
super(List.of(uri), retry); super(List.of(NetworkUtils.toURI(uri)));
} }
@Override @Override

View File

@@ -17,6 +17,8 @@
*/ */
package org.jackhuang.hmcl.download; package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -33,7 +35,7 @@ public interface DownloadProvider {
String getAssetBaseURL(); String getAssetBaseURL();
default List<URI> getAssetObjectCandidates(String assetObjectLocation) { default List<URI> getAssetObjectCandidates(String assetObjectLocation) {
return List.of(URI.create(getAssetBaseURL() + assetObjectLocation)); return List.of(NetworkUtils.toURI(getAssetBaseURL() + assetObjectLocation));
} }
/** /**
@@ -57,7 +59,7 @@ public interface DownloadProvider {
* @return the URL that is equivalent to [baseURL], but belongs to your own service provider. * @return the URL that is equivalent to [baseURL], but belongs to your own service provider.
*/ */
default List<URI> injectURLWithCandidates(String baseURL) { default List<URI> injectURLWithCandidates(String baseURL) {
return List.of(URI.create(injectURL(baseURL))); return List.of(NetworkUtils.toURI(injectURL(baseURL)));
} }
default List<URI> injectURLsWithCandidates(List<String> urls) { default List<URI> injectURLsWithCandidates(List<String> urls) {

View File

@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -59,7 +58,7 @@ public final class FabricAPIInstallTask extends Task<Version> {
@Override @Override
public void execute() throws IOException { public void execute() throws IOException {
dependencies.add(new FileDownloadTask( dependencies.add(new FileDownloadTask(
URI.create(remote.getVersion().getFile().getUrl()), remote.getVersion().getFile().getUrl(),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getVersion().getVersion() + ".jar"), dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getVersion().getVersion() + ".jar"),
remote.getVersion().getFile().getIntegrityCheck()) remote.getVersion().getFile().getIntegrityCheck())
); );

View File

@@ -29,7 +29,6 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.time.Instant; import java.time.Instant;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -86,7 +85,7 @@ public final class ForgeBMCLVersionList extends VersionList<ForgeRemoteVersion>
public Task<?> refreshAsync(String gameVersion) { public Task<?> refreshAsync(String gameVersion) {
String lookupVersion = toLookupVersion(gameVersion); String lookupVersion = toLookupVersion(gameVersion);
return new GetTask(URI.create(apiRoot + "/forge/minecraft/" + lookupVersion)).thenGetJsonAsync(listTypeOf(ForgeVersion.class)) return new GetTask(apiRoot + "/forge/minecraft/" + lookupVersion).thenGetJsonAsync(listTypeOf(ForgeVersion.class))
.thenAcceptAsync(forgeVersions -> { .thenAcceptAsync(forgeVersions -> {
lock.writeLock().lock(); lock.writeLock().lock();
try { try {

View File

@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.net.URI;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -54,7 +53,7 @@ public final class GameVersionList extends VersionList<GameRemoteVersion> {
@Override @Override
public Task<?> refreshAsync() { public Task<?> refreshAsync() {
return new GetTask(URI.create(downloadProvider.getVersionListURL())).thenGetJsonAsync(GameRemoteVersions.class) return new GetTask(downloadProvider.getVersionListURL()).thenGetJsonAsync(GameRemoteVersions.class)
.thenAcceptAsync(root -> { .thenAcceptAsync(root -> {
GameRemoteVersions unlistedVersions = null; GameRemoteVersions unlistedVersions = null;

View File

@@ -28,7 +28,6 @@ import org.jsoup.nodes.Document;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -53,7 +52,7 @@ public final class LiteLoaderVersionList extends VersionList<LiteLoaderRemoteVer
@Override @Override
public Task<?> refreshAsync(String gameVersion) { public Task<?> refreshAsync(String gameVersion) {
return new GetTask(URI.create(downloadProvider.injectURL(LITELOADER_LIST))) return new GetTask(downloadProvider.injectURL(LITELOADER_LIST))
.thenGetJsonAsync(LiteLoaderVersionsRoot.class) .thenGetJsonAsync(LiteLoaderVersionsRoot.class)
.thenAcceptAsync(root -> { .thenAcceptAsync(root -> {
LiteLoaderGameVersions versions = root.getVersions().get(gameVersion); LiteLoaderGameVersions versions = root.getVersions().get(gameVersion);

View File

@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.Optional; import java.util.Optional;
@@ -66,7 +65,7 @@ public final class NeoForgeBMCLVersionList extends VersionList<NeoForgeRemoteVer
@Override @Override
public Task<?> refreshAsync(String gameVersion) { public Task<?> refreshAsync(String gameVersion) {
return new GetTask(URI.create(apiRoot + "/neoforge/list/" + gameVersion)).thenGetJsonAsync(listTypeOf(NeoForgeVersion.class)) return new GetTask(apiRoot + "/neoforge/list/" + gameVersion).thenGetJsonAsync(listTypeOf(NeoForgeVersion.class))
.thenAcceptAsync(neoForgeVersions -> { .thenAcceptAsync(neoForgeVersions -> {
lock.writeLock().lock(); lock.writeLock().lock();

View File

@@ -5,7 +5,6 @@ import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -39,8 +38,8 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
@Override @Override
public Task<?> refreshAsync() { public Task<?> refreshAsync() {
return Task.allOf( return Task.allOf(
new GetTask(URI.create(downloadProvider.injectURL(OLD_URL))).thenGetJsonAsync(OfficialAPIResult.class), new GetTask(downloadProvider.injectURL(OLD_URL)).thenGetJsonAsync(OfficialAPIResult.class),
new GetTask(URI.create(downloadProvider.injectURL(META_URL))).thenGetJsonAsync(OfficialAPIResult.class) new GetTask(downloadProvider.injectURL(META_URL)).thenGetJsonAsync(OfficialAPIResult.class)
).thenAcceptAsync(results -> { ).thenAcceptAsync(results -> {
lock.writeLock().lock(); lock.writeLock().lock();

View File

@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -73,7 +72,7 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
@Override @Override
public Task<?> refreshAsync() { public Task<?> refreshAsync() {
return new GetTask(URI.create(apiRoot + "/optifine/versionlist")).thenGetJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> { return new GetTask(apiRoot + "/optifine/versionlist").thenGetJsonAsync(listTypeOf(OptiFineVersion.class)).thenAcceptAsync(root -> {
lock.writeLock().lock(); lock.writeLock().lock();
try { try {

View File

@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -59,7 +58,7 @@ public final class QuiltAPIInstallTask extends Task<Version> {
@Override @Override
public void execute() throws IOException { public void execute() throws IOException {
dependencies.add(new FileDownloadTask( dependencies.add(new FileDownloadTask(
URI.create(remote.getVersion().getFile().getUrl()), remote.getVersion().getFile().getUrl(),
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("quilt-api-" + remote.getVersion().getVersion() + ".jar"), dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("quilt-api-" + remote.getVersion().getVersion() + ".jar"),
remote.getVersion().getFile().getIntegrityCheck()) remote.getVersion().getFile().getIntegrityCheck())
); );

View File

@@ -28,9 +28,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.io.NetworkUtils.encodeLocation; public final class RemoteMod {
public class RemoteMod {
public static final RemoteMod BROKEN = new RemoteMod("", "", "RemoteMod.BROKEN", "", Collections.emptyList(), "", "", new RemoteMod.IMod() { public static final RemoteMod BROKEN = new RemoteMod("", "", "RemoteMod.BROKEN", "", Collections.emptyList(), "", "", new RemoteMod.IMod() {
@Override @Override
@@ -313,7 +311,7 @@ public class RemoteMod {
} }
public String getUrl() { public String getUrl() {
return encodeLocation(url); return url;
} }
public String getFilename() { public String getFilename() {

View File

@@ -21,10 +21,8 @@ import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.util.Objects; import java.util.Objects;
/** /**
@@ -84,15 +82,13 @@ public final class CurseManifestFile implements Validation {
} }
@Nullable @Nullable
public URI getUrl() { public String getUrl() {
if (url == null) { if (url == null) {
if (fileName != null) { return fileName != null
return URI.create(NetworkUtils.encodeLocation(String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName))); ? String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName)
: null;
} else { } else {
return null; return url;
}
} else {
return URI.create(NetworkUtils.encodeLocation(url));
} }
} }

View File

@@ -35,7 +35,6 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -101,7 +100,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
throw new CustomException(); throw new CustomException();
} }
})).thenComposeAsync(wrap(unused1 -> { })).thenComposeAsync(wrap(unused1 -> {
return executor.one(new GetTask(URI.create(manifest.getFileApi() + "/manifest.json"))); return executor.one(new GetTask(manifest.getFileApi() + "/manifest.json"));
})).thenComposeAsync(wrap(remoteManifestJson -> { })).thenComposeAsync(wrap(remoteManifestJson -> {
McbbsModpackManifest remoteManifest; McbbsModpackManifest remoteManifest;
// We needs to update modpack from online server. // We needs to update modpack from online server.
@@ -201,10 +200,10 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
McbbsModpackManifest.CurseFile file = (McbbsModpackManifest.CurseFile) rawFile; McbbsModpackManifest.CurseFile file = (McbbsModpackManifest.CurseFile) rawFile;
if (StringUtils.isBlank(file.getFileName())) { if (StringUtils.isBlank(file.getFileName())) {
try { try {
return file.withFileName(NetworkUtils.detectFileName(file.getUrl())); return file.withFileName(NetworkUtils.detectFileName(NetworkUtils.toURI(file.getUrl())));
} catch (IOException e) { } catch (IOException e) {
try { try {
String result = NetworkUtils.doGet(URI.create(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID()))); String result = NetworkUtils.doGet(String.format("https://cursemeta.dries007.net/%d/%d.json", file.getProjectID(), file.getFileID()));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class); CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL()); return file.withFileName(mod.getFileNameOnDisk()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) { } catch (FileNotFoundException fof) {
@@ -213,7 +212,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
return file; return file;
} catch (IOException | JsonParseException e2) { } catch (IOException | JsonParseException e2) {
try { try {
String result = NetworkUtils.doGet(URI.create(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID()))); String result = NetworkUtils.doGet(String.format("https://addons-ecs.forgesvc.net/api/v2/addon/%d/file/%d", file.getProjectID(), file.getFileID()));
CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class); CurseMetaMod mod = JsonUtils.fromNonNullJson(result, CurseMetaMod.class);
return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL()); return file.withFileName(mod.getFileName()).withURL(mod.getDownloadURL());
} catch (FileNotFoundException fof) { } catch (FileNotFoundException fof) {
@@ -297,7 +296,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
if (file instanceof McbbsModpackManifest.AddonFile) { if (file instanceof McbbsModpackManifest.AddonFile) {
McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file; McbbsModpackManifest.AddonFile addonFile = (McbbsModpackManifest.AddonFile) file;
return new FileDownloadTask( return new FileDownloadTask(
URI.create(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(addonFile.getPath())), remoteManifest.getFileApi() + "/overrides/" + addonFile.getPath(),
modManager.getSimpleModPath(addonFile.getPath()), modManager.getSimpleModPath(addonFile.getPath()),
addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null); addonFile.getHash() != null ? new FileDownloadTask.IntegrityCheck("SHA-1", addonFile.getHash()) : null);
} else if (file instanceof McbbsModpackManifest.CurseFile) { } else if (file instanceof McbbsModpackManifest.CurseFile) {

View File

@@ -27,11 +27,9 @@ import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider; import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.*; import org.jackhuang.hmcl.util.gson.*;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -329,9 +327,10 @@ public class McbbsModpackManifest implements ModpackManifest, Validation {
return fileName; return fileName;
} }
public URI getUrl() { public String getUrl() {
return url == null ? URI.create("https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file") return url == null
: URI.create(NetworkUtils.encodeLocation(url)); ? "https://www.curseforge.com/minecraft/mc-mods/" + projectID + "/download/" + fileID + "/file"
: url;
} }
public CurseFile withFileName(String fileName) { public CurseFile withFileName(String fileName) {

View File

@@ -1,6 +1,7 @@
package org.jackhuang.hmcl.mod.multimc; package org.jackhuang.hmcl.mod.multimc;
import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.net.URI; import java.net.URI;
import java.util.*; import java.util.*;
@@ -46,6 +47,6 @@ public final class MultiMCComponents {
} }
public static URI getMetaURL(String componentID, String version) { public static URI getMetaURL(String componentID, String version) {
return URI.create(String.format("https://meta.multimc.org/v1/%s/%s.json", componentID, version)); return NetworkUtils.toURI(String.format("https://meta.multimc.org/v1/%s/%s.json", componentID, version));
} }
} }

View File

@@ -28,11 +28,9 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@@ -84,7 +82,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
@Override @Override
public void preExecute() throws Exception { public void preExecute() throws Exception {
if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return; if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return;
dependent = new GetTask(URI.create(manifest.getManifest().getFileApi() + "/server-manifest.json")); dependent = new GetTask(manifest.getManifest().getFileApi() + "/server-manifest.json");
} }
@Override @Override
@@ -152,7 +150,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
if (download) { if (download) {
total++; total++;
dependencies.add(new FileDownloadTask( dependencies.add(new FileDownloadTask(
URI.create(remoteManifest.getFileApi() + "/overrides/" + NetworkUtils.encodeLocation(file.getPath())), remoteManifest.getFileApi() + "/overrides/" + file.getPath(),
actualPath, actualPath,
new FileDownloadTask.IntegrityCheck("SHA-1", file.getHash())) new FileDownloadTask.IntegrityCheck("SHA-1", file.getHash()))
.withCounter("hmcl.modpack.download")); .withCounter("hmcl.modpack.download"));

View File

@@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl.task; package org.jackhuang.hmcl.task;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
@@ -36,12 +37,12 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
*/ */
public final class CacheFileTask extends FetchTask<Path> { public final class CacheFileTask extends FetchTask<Path> {
public CacheFileTask(@NotNull URI uri) { public CacheFileTask(@NotNull String uri) {
super(List.of(uri), DEFAULT_RETRY); super(List.of(NetworkUtils.toURI(uri)));
} }
public CacheFileTask(@NotNull URI uri, int retry) { public CacheFileTask(@NotNull URI uri) {
super(List.of(uri), retry); super(List.of(uri));
} }
@Override @Override

View File

@@ -49,14 +49,13 @@ public abstract class FetchTask<T> extends Task<T> {
protected static final int DEFAULT_RETRY = 3; protected static final int DEFAULT_RETRY = 3;
protected final List<URI> uris; protected final List<URI> uris;
protected final int retry; protected int retry = DEFAULT_RETRY;
protected CacheRepository repository = CacheRepository.getInstance(); protected CacheRepository repository = CacheRepository.getInstance();
public FetchTask(@NotNull List<@NotNull URI> uris, int retry) { public FetchTask(@NotNull List<@NotNull URI> uris) {
Objects.requireNonNull(uris); Objects.requireNonNull(uris);
this.uris = List.copyOf(uris); this.uris = List.copyOf(uris);
this.retry = retry;
if (this.uris.isEmpty()) if (this.uris.isEmpty())
throw new IllegalArgumentException("At least one URL is required"); throw new IllegalArgumentException("At least one URL is required");
@@ -64,6 +63,13 @@ public abstract class FetchTask<T> extends Task<T> {
setExecutor(download()); setExecutor(download());
} }
public void setRetry(int retry) {
if (retry <= 0)
throw new IllegalArgumentException("Retry count must be greater than 0");
this.retry = retry;
}
public void setCacheRepository(CacheRepository repository) { public void setCacheRepository(CacheRepository repository) {
this.repository = repository; this.repository = repository;
} }
@@ -167,7 +173,7 @@ public abstract class FetchTask<T> extends Task<T> {
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
// Handle cache // Handle cache
try { try {
Path cache = repository.getCachedRemoteFile(conn.getURL().toURI()); Path cache = repository.getCachedRemoteFile(NetworkUtils.toURI(conn.getURL()));
useCachedResult(cache); useCachedResult(cache);
LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri)); LOG.info("Using cached file for " + NetworkUtils.dropQuery(uri));
return; return;

View File

@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException; import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@@ -82,6 +83,23 @@ public class FileDownloadTask extends FetchTask<Void> {
private Path candidate; private Path candidate;
private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>(); private final ArrayList<IntegrityCheckHandler> integrityCheckHandlers = new ArrayList<>();
/**
* @param uri the URI of remote file.
* @param path the location that download to.
*/
public FileDownloadTask(String uri, Path path) {
this(List.of(NetworkUtils.toURI(uri)), path, null);
}
/**
* @param uri the URI of remote file.
* @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
*/
public FileDownloadTask(String uri, Path path, IntegrityCheck integrityCheck) {
this(List.of(NetworkUtils.toURI(uri)), path, integrityCheck);
}
/** /**
* @param uri the URI of remote file. * @param uri the URI of remote file.
* @param path the location that download to. * @param path the location that download to.
@@ -99,16 +117,6 @@ public class FileDownloadTask extends FetchTask<Void> {
this(List.of(uri), path, integrityCheck); this(List.of(uri), path, integrityCheck);
} }
/**
* @param uri the URI of remote file.
* @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
* @param retry the times for retrying if downloading fails.
*/
public FileDownloadTask(URI uri, Path path, IntegrityCheck integrityCheck, int retry) {
this(List.of(uri), path, integrityCheck, retry);
}
/** /**
* Constructor. * Constructor.
* *
@@ -127,19 +135,7 @@ public class FileDownloadTask extends FetchTask<Void> {
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed * @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
*/ */
public FileDownloadTask(List<URI> uris, Path path, IntegrityCheck integrityCheck) { public FileDownloadTask(List<URI> uris, Path path, IntegrityCheck integrityCheck) {
this(uris, path, integrityCheck, DEFAULT_RETRY); super(uris);
}
/**
* Constructor.
*
* @param uris uris of remote file, will be attempted in order.
* @param path the location that download to.
* @param integrityCheck the integrity check to perform, null if no integrity check is to be performed
* @param retry the times for retrying if downloading fails.
*/
public FileDownloadTask(List<URI> uris, Path path, IntegrityCheck integrityCheck, int retry) {
super(uris, retry);
this.file = path; this.file = path;
this.integrityCheck = integrityCheck; this.integrityCheck = integrityCheck;

View File

@@ -19,12 +19,12 @@ package org.jackhuang.hmcl.task;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
@@ -36,29 +36,18 @@ import static java.nio.charset.StandardCharsets.UTF_8;
*/ */
public final class GetTask extends FetchTask<String> { public final class GetTask extends FetchTask<String> {
private final Charset charset; public GetTask(String uri) {
this(NetworkUtils.toURI(uri));
}
public GetTask(URI url) { public GetTask(URI url) {
this(url, UTF_8); this(List.of(url));
} setName(url.toString());
public GetTask(URI url, Charset charset) {
this(url, charset, DEFAULT_RETRY);
}
public GetTask(URI url, Charset charset, int retry) {
this(List.of(url), charset, retry);
} }
public GetTask(List<URI> url) { public GetTask(List<URI> url) {
this(url, UTF_8, DEFAULT_RETRY); super(url);
} setName(url.get(0).toString());
public GetTask(List<URI> urls, Charset charset, int retry) {
super(urls, retry);
this.charset = charset;
setName(urls.get(0).toString());
} }
@Override @Override
@@ -85,7 +74,7 @@ public final class GetTask extends FetchTask<String> {
public void close() throws IOException { public void close() throws IOException {
if (!isSuccess()) return; if (!isSuccess()) return;
String result = baos.toString(charset); String result = baos.toString(UTF_8);
setResult(result); setResult(result);
if (checkETag) { if (checkETag) {

View File

@@ -29,7 +29,6 @@ import org.jetbrains.annotations.Nullable;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
@@ -185,8 +184,8 @@ public class CacheRepository {
URI uri; URI uri;
try { try {
uri = NetworkUtils.dropQuery(conn.getURL().toURI()); uri = NetworkUtils.dropQuery(NetworkUtils.toURI(conn.getURL()));
} catch (URISyntaxException e) { } catch (IllegalArgumentException e) {
return; return;
} }
ETagItem eTagItem; ETagItem eTagItem;
@@ -229,16 +228,16 @@ public class CacheRepository {
if (StringUtils.isBlank(eTag)) return null; if (StringUtils.isBlank(eTag)) return null;
URI uri; URI uri;
try { try {
uri = NetworkUtils.dropQuery(connection.getURL().toURI()); uri = NetworkUtils.dropQuery(NetworkUtils.toURI(connection.getURL()));
} catch (URISyntaxException e) { } catch (IllegalArgumentException e) {
throw new IOException(e); throw new IOException(e);
} }
String lastModified = connection.getHeaderField("Last-Modified"); String lastModified = connection.getHeaderField("Last-Modified");
CacheResult cacheResult = cacheSupplier.get(); CacheResult cacheResult = cacheSupplier.get();
ETagItem eTagItem = new ETagItem(uri, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified); ETagItem eTagItem = new ETagItem(uri.toString(), eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified);
lock.writeLock().lock(); lock.writeLock().lock();
try { try {
index.compute(eTagItem.url, updateEntity(eTagItem, true)); index.compute(uri, updateEntity(eTagItem, true));
saveETagIndex(); saveETagIndex();
} finally { } finally {
lock.writeLock().unlock(); lock.writeLock().unlock();
@@ -282,7 +281,7 @@ public class CacheRepository {
for (Collection<ETagItem> eTagItems : indexes) { for (Collection<ETagItem> eTagItems : indexes) {
if (eTagItems != null) { if (eTagItems != null) {
for (ETagItem eTag : eTagItems) { for (ETagItem eTag : eTagItems) {
eTags.compute(eTag.url, updateEntity(eTag, false)); eTags.compute(NetworkUtils.toURI(eTag.url), updateEntity(eTag, false));
} }
} }
} }
@@ -331,7 +330,7 @@ public class CacheRepository {
} }
private static final class ETagItem { private static final class ETagItem {
private final URI url; private final String url;
private final String eTag; private final String eTag;
private final String hash; private final String hash;
@SerializedName("local") @SerializedName("local")
@@ -346,7 +345,7 @@ public class CacheRepository {
this(null, null, null, 0, null); this(null, null, null, 0, null);
} }
public ETagItem(URI url, String eTag, String hash, long localLastModified, String remoteLastModified) { public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified) {
this.url = url; this.url = url;
this.eTag = eTag; this.eTag = eTag;
this.hash = hash; this.hash = hash;

View File

@@ -29,7 +29,6 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -120,7 +119,7 @@ public abstract class HttpRequest {
} }
public HttpURLConnection createConnection() throws IOException { public HttpURLConnection createConnection() throws IOException {
HttpURLConnection con = createHttpConnection(URI.create(url)); HttpURLConnection con = createHttpConnection(url);
con.setRequestMethod(method); con.setRequestMethod(method);
for (Map.Entry<String, String> entry : headers.entrySet()) { for (Map.Entry<String, String> entry : headers.entrySet()) {
con.setRequestProperty(entry.getKey(), entry.getValue()); con.setRequestProperty(entry.getKey(), entry.getValue());
@@ -188,9 +187,9 @@ public abstract class HttpRequest {
if (con.getResponseCode() / 100 != 2) { if (con.getResponseCode() / 100 != 2) {
if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) { if (!ignoreHttpCode && !toleratedHttpCodes.contains(con.getResponseCode())) {
try { try {
throw new ResponseCodeException(NetworkUtils.toURI(url), con.getResponseCode(), NetworkUtils.readFullyAsString(con)); throw new ResponseCodeException(url.toString(), con.getResponseCode(), NetworkUtils.readFullyAsString(con));
} catch (IOException e) { } catch (IOException e) {
throw new ResponseCodeException(NetworkUtils.toURI(url), con.getResponseCode(), e); throw new ResponseCodeException(url.toString(), con.getResponseCode(), e);
} }
} }
} }

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util.io; package org.jackhuang.hmcl.util.io;
import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.Pair;
import org.jetbrains.annotations.NotNull;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
@@ -122,10 +123,18 @@ public final class NetworkUtils {
return connection; return connection;
} }
public static HttpURLConnection createHttpConnection(String url) throws IOException {
return (HttpURLConnection) createConnection(toURI(url));
}
public static HttpURLConnection createHttpConnection(URI url) throws IOException { public static HttpURLConnection createHttpConnection(URI url) throws IOException {
return (HttpURLConnection) createConnection(url); return (HttpURLConnection) createConnection(url);
} }
private static void encodeCodePoint(StringBuilder builder, int codePoint) {
builder.append(encodeURL(Character.toString(codePoint)));
}
/** /**
* @param location the url to be URL encoded * @param location the url to be URL encoded
* @return encoded URL * @return encoded URL
@@ -133,29 +142,59 @@ public final class NetworkUtils {
* "https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a> * "https://github.com/curl/curl/blob/3f7b1bb89f92c13e69ee51b710ac54f775aab320/lib/transfer.c#L1427-L1461">Curl</a>
*/ */
public static String encodeLocation(String location) { public static String encodeLocation(String location) {
StringBuilder sb = new StringBuilder(); int i = 0;
boolean left = true; boolean left = true;
for (char ch : location.toCharArray()) { while (i < location.length()) {
switch (ch) { char ch = location.charAt(i);
case ' ': if (ch == ' ' || ch >= 0x80)
if (left)
sb.append("%20");
else
sb.append('+');
break; break;
case '?': else if (ch == '?')
left = false; left = false;
// fallthrough i++;
default: }
if (ch >= 0x80)
sb.append(encodeURL(Character.toString(ch))); if (i == location.length()) {
else // No need to encode
sb.append(ch); return location;
break; }
var builder = new StringBuilder(location.length() + 10);
builder.append(location, 0, i);
for (; i < location.length(); i++) {
char ch = location.charAt(i);
if (Character.isSurrogate(ch)) {
if (Character.isHighSurrogate(ch) && i < location.length() - 1) {
char ch2 = location.charAt(i + 1);
if (Character.isLowSurrogate(ch2)) {
int codePoint = Character.toCodePoint(ch, ch2);
encodeCodePoint(builder, codePoint);
i++;
continue;
} }
} }
return sb.toString(); // Invalid surrogate pair, encode as '?'
builder.append("%3F");
continue;
}
if (ch == ' ') {
if (left)
builder.append("%20");
else
builder.append('+');
} else if (ch == '?') {
left = false;
builder.append('?');
} else if (ch >= 0x80) {
encodeCodePoint(builder, ch);
} else {
builder.append(ch);
}
}
return builder.toString();
} }
/** /**
@@ -200,6 +239,10 @@ public final class NetworkUtils {
return conn; return conn;
} }
public static String doGet(String uri) throws IOException {
return doGet(toURI(uri));
}
public static String doGet(URI uri) throws IOException { public static String doGet(URI uri) throws IOException {
return readFullyAsString(resolveConnection(createHttpConnection(uri))); return readFullyAsString(resolveConnection(createHttpConnection(uri)));
} }
@@ -319,14 +362,6 @@ public final class NetworkUtils {
} }
} }
public static URI toURI(URL url) {
try {
return url.toURI();
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
// ==== Shortcut methods for encoding/decoding URLs in UTF-8 ==== // ==== Shortcut methods for encoding/decoding URLs in UTF-8 ====
public static String encodeURL(String toEncode) { public static String encodeURL(String toEncode) {
return URLEncoder.encode(toEncode, UTF_8); return URLEncoder.encode(toEncode, UTF_8);
@@ -335,6 +370,20 @@ public final class NetworkUtils {
public static String decodeURL(String toDecode) { public static String decodeURL(String toDecode) {
return URLDecoder.decode(toDecode, UTF_8); return URLDecoder.decode(toDecode, UTF_8);
} }
/// @throws IllegalArgumentException if the string is not a valid URI
public static @NotNull URI toURI(@NotNull String uri) {
try {
return new URI(encodeLocation(uri));
} catch (URISyntaxException e) {
// Possibly an Internationalized Domain Name (IDN)
return URI.create(uri);
}
}
public static @NotNull URI toURI(@NotNull URL url) {
return toURI(url.toExternalForm());
}
// ==== // ====
} }

View File

@@ -22,32 +22,44 @@ import java.net.URI;
public final class ResponseCodeException extends IOException { public final class ResponseCodeException extends IOException {
private final URI uri; private final String uri;
private final int responseCode; private final int responseCode;
private final String data; private final String data;
public ResponseCodeException(URI uri, int responseCode) { public ResponseCodeException(URI uri, int responseCode) {
this(uri.toString(), responseCode);
}
public ResponseCodeException(URI uri, int responseCode, Throwable cause) {
this(uri.toString(), responseCode, cause);
}
public ResponseCodeException(URI uri, int responseCode, String data) {
this(uri.toString(), responseCode, data);
}
public ResponseCodeException(String uri, int responseCode) {
super("Unable to request url " + uri + ", response code: " + responseCode); super("Unable to request url " + uri + ", response code: " + responseCode);
this.uri = uri; this.uri = uri;
this.responseCode = responseCode; this.responseCode = responseCode;
this.data = null; this.data = null;
} }
public ResponseCodeException(URI uri, int responseCode, Throwable cause) { public ResponseCodeException(String uri, int responseCode, Throwable cause) {
super("Unable to request url " + uri + ", response code: " + responseCode, cause); super("Unable to request url " + uri + ", response code: " + responseCode, cause);
this.uri = uri; this.uri = uri;
this.responseCode = responseCode; this.responseCode = responseCode;
this.data = null; this.data = null;
} }
public ResponseCodeException(URI uri, int responseCode, String data) { public ResponseCodeException(String uri, int responseCode, String data) {
super("Unable to request url " + uri + ", response code: " + responseCode + ", data: " + data); super("Unable to request url " + uri + ", response code: " + responseCode + ", data: " + data);
this.uri = uri; this.uri = uri;
this.responseCode = responseCode; this.responseCode = responseCode;
this.data = data; this.data = data;
} }
public URI getUri() { public String getUri() {
return uri; return uri;
} }

View File

@@ -21,18 +21,39 @@ import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.io.NetworkUtils.encodeLocation;
import static org.jackhuang.hmcl.util.io.NetworkUtils.getCharsetFromContentType;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
/** /**
* @author Glavo * @author Glavo
*/ */
public class NetworkUtilsTest { public class NetworkUtilsTest {
@Test
public void testEncodeLocation() {
assertEquals("https://github.com", encodeLocation("https://github.com"));
assertEquals("https://github.com/HMCL-dev/HMCL/commits?author=Glavo", encodeLocation("https://github.com/HMCL-dev/HMCL/commits?author=Glavo"));
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file with space"));
assertEquals("https://www.example.com/file%20with%20space", encodeLocation("https://www.example.com/file%20with%20space"));
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95", encodeLocation("https://www.example.com/测试"));
assertEquals("https://www.example.com/%F0%9F%98%87", encodeLocation("https://www.example.com/\uD83D\uDE07"));
assertEquals("https://www.example.com/test?a=10+20", encodeLocation("https://www.example.com/test?a=10 20"));
assertEquals("https://www.example.com/%E6%B5%8B%E8%AF%95?a=10+20", encodeLocation("https://www.example.com/测试?a=10 20"));
// Invalid surrogate pair
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uD83D"));
assertEquals("https://www.example.com/%3F", encodeLocation("https://www.example.com/\uDE07"));
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uD83Dtest"));
assertEquals("https://www.example.com/%3Ftest", encodeLocation("https://www.example.com/\uDE07test"));
}
@Test @Test
public void testGetEncodingFromUrl() { public void testGetEncodingFromUrl() {
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType(null)); assertEquals(UTF_8, getCharsetFromContentType(null));
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType("")); assertEquals(UTF_8, getCharsetFromContentType(""));
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType("text/html")); assertEquals(UTF_8, getCharsetFromContentType("text/html"));
assertEquals(UTF_8, NetworkUtils.getCharsetFromContentType("text/html; charset=utf-8")); assertEquals(UTF_8, getCharsetFromContentType("text/html; charset=utf-8"));
assertEquals(US_ASCII, NetworkUtils.getCharsetFromContentType("text/html; charset=ascii")); assertEquals(US_ASCII, getCharsetFromContentType("text/html; charset=ascii"));
} }
} }