From 5a8d567bd783346d86187cf631656fd15b3acc5a Mon Sep 17 00:00:00 2001 From: Glavo Date: Tue, 16 Sep 2025 16:11:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=20isNameValid=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E8=87=B3=20FileUtils=20(#4491)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/game/HMCLGameRepository.java | 2 +- .../hmcl/ui/download/DownloadPage.java | 4 +- .../hmcl/ui/versions/SchematicsPage.java | 3 +- .../jackhuang/hmcl/ui/versions/Versions.java | 3 +- .../org/jackhuang/hmcl/util/io/FileUtils.java | 78 +++++++++++++++++ .../hmcl/util/platform/OperatingSystem.java | 86 ------------------- .../jackhuang/hmcl/util/io/FileUtilsTest.java | 70 +++++++++++++++ .../util/platform/OperatingSystemTest.java | 55 ------------ 8 files changed, 154 insertions(+), 147 deletions(-) create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/FileUtilsTest.java delete mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/OperatingSystemTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index c2608dcbc..28564f0af 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -489,7 +489,7 @@ public class HMCLGameRepository extends DefaultGameRepository { FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT))) return false; - return OperatingSystem.isNameValid(id); + return FileUtils.isNameValid(id); } /** diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index aee357f7b..c3d6ed011 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -52,7 +52,7 @@ import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.util.TaskCancellationAction; -import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; import java.nio.file.Path; @@ -148,7 +148,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath(); Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> { - if (!OperatingSystem.isNameValid(result)) { + if (!FileUtils.isNameValid(result)) { reject.accept(i18n("install.new_game.malformed")); return; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 2586f34eb..c383773cb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -42,7 +42,6 @@ import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -170,7 +169,7 @@ public final class SchematicsPage extends ListPageBase impl return; } - if (result.contains("/") || result.contains("\\") || !OperatingSystem.isNameValid(result)) { + if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) { reject.accept(i18n("schematics.create_directory.failed.invalid_name")); return; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index aa95dd6c6..50bb72824 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -40,6 +40,7 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.export.ExportWizardProvider; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -119,7 +120,7 @@ public final class Versions { public static CompletableFuture renameVersion(Profile profile, String version) { return Controllers.prompt(i18n("version.manage.rename.message"), (newName, resolve, reject) -> { - if (!OperatingSystem.isNameValid(newName)) { + if (!FileUtils.isNameValid(newName)) { reject.accept(i18n("install.new_game.malformed")); return; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java index 6e4a74334..61ef87b73 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java @@ -114,6 +114,84 @@ public final class FileUtils { else return getName(path); } + // https://learn.microsoft.com/biztalk/core/restrictions-when-configuring-the-file-adapter + private static final Set INVALID_WINDOWS_RESOURCE_BASE_NAMES = Set.of( + "aux", "con", "nul", "prn", "clock$", + "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8", "com9", + "com¹", "com²", "com³", + "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", + "lpt¹", "lpt²", "lpt³" + ); + + /// @see #isNameValid(OperatingSystem, String) + public static boolean isNameValid(String name) { + return isNameValid(OperatingSystem.CURRENT_OS, name); + } + + /// Returns true if the given name is a valid file name on the given operating system, + /// and `false` otherwise. + public static boolean isNameValid(OperatingSystem os, String name) { + // empty filename is not allowed + if (name.isEmpty()) + return false; + // '.', '..' and '~' have special meaning on all platforms + if (name.equals(".") || name.equals("..") || name.equals("~")) + return false; + + for (int i = 0; i < name.length(); i++) { + char ch = name.charAt(i); + int codePoint; + + if (Character.isSurrogate(ch)) { + if (!Character.isHighSurrogate(ch)) + return false; + + if (i == name.length() - 1) + return false; + + char ch2 = name.charAt(++i); + if (!Character.isLowSurrogate(ch2)) + return false; + + codePoint = Character.toCodePoint(ch, ch2); + } else { + codePoint = ch; + } + + if (!Character.isValidCodePoint(codePoint) + || Character.isISOControl(codePoint) + || codePoint == '/' || codePoint == '\0' + // Unicode replacement character + || codePoint == 0xfffd + // Not Unicode character + || codePoint == 0xfffe || codePoint == 0xffff) + return false; + + // https://learn.microsoft.com/windows/win32/fileio/naming-a-file + if (os == OperatingSystem.WINDOWS && + (ch == '<' || ch == '>' || ch == ':' || ch == '"' || ch == '\\' || ch == '|' || ch == '?' || ch == '*')) { + return false; + } + } + + if (os == OperatingSystem.WINDOWS) { // Windows only + char lastChar = name.charAt(name.length() - 1); + // filenames ending in dot are not valid + if (lastChar == '.') + return false; + // file names ending with whitespace are truncated (bug 118997) + if (Character.isWhitespace(lastChar)) + return false; + + // on windows, filename suffixes are not relevant to name validity + String basename = StringUtils.substringBeforeLast(name, '.'); + if (INVALID_WINDOWS_RESOURCE_BASE_NAMES.contains(basename.toLowerCase(Locale.ROOT))) + return false; + } + + return true; + } + public static String readTextMaybeNativeEncoding(Path file) throws IOException { byte[] bytes = Files.readAllBytes(file); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java index a01a28766..73e82a7b8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java @@ -121,10 +121,6 @@ public enum OperatingSystem { public static final int CODE_PAGE; - public static final Pattern INVALID_RESOURCE_CHARACTERS; - private static final String[] INVALID_RESOURCE_BASENAMES; - private static final String[] INVALID_RESOURCE_FULLNAMES; - static { String nativeEncoding = System.getProperty("native.encoding"); String hmclNativeEncoding = System.getProperty("hmcl.native.encoding"); @@ -229,24 +225,6 @@ public enum OperatingSystem { } OS_RELEASE_NAME = osRelease.get("NAME"); OS_RELEASE_PRETTY_NAME = osRelease.get("PRETTY_NAME"); - - // setup the invalid names - if (CURRENT_OS == WINDOWS) { - // valid names and characters taken from http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/fs/naming_a_file.asp - INVALID_RESOURCE_CHARACTERS = Pattern.compile("[/\"<>|?*:\\\\]"); - INVALID_RESOURCE_BASENAMES = new String[]{"aux", "com1", "com2", "com3", "com4", - "com5", "com6", "com7", "com8", "com9", "con", "lpt1", "lpt2", - "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9", "nul", "prn"}; - Arrays.sort(INVALID_RESOURCE_BASENAMES); - //CLOCK$ may be used if an extension is provided - INVALID_RESOURCE_FULLNAMES = new String[]{"clock$"}; - } else { - //only front slash and null char are invalid on UNIXes - //taken from http://www.faqs.org/faqs/unix-faq/faq/part2/section-2.html - INVALID_RESOURCE_CHARACTERS = null; - INVALID_RESOURCE_BASENAMES = null; - INVALID_RESOURCE_FULLNAMES = null; - } } public static OperatingSystem parseOSName(String name) { @@ -288,68 +266,4 @@ public enum OperatingSystem { } } - /** - * Returns true if the given name is a valid file name on this operating system, - * and false otherwise. - */ - public static boolean isNameValid(String name) { - // empty filename is not allowed - if (name.isEmpty()) - return false; - // . and .. have special meaning on all platforms - if (name.equals(".") || name.equals("..")) - return false; - - for (int i = 0; i < name.length(); i++) { - char ch = name.charAt(i); - int codePoint; - - if (Character.isSurrogate(ch)) { - if (!Character.isHighSurrogate(ch)) - return false; - - if (i == name.length() - 1) - return false; - - char ch2 = name.charAt(++i); - if (!Character.isLowSurrogate(ch2)) - return false; - - codePoint = Character.toCodePoint(ch, ch2); - } else { - codePoint = ch; - } - - if (!Character.isValidCodePoint(codePoint) - || Character.isISOControl(codePoint) - || codePoint == '/' || codePoint == '\0' - // Unicode replacement character - || codePoint == 0xfffd - // Not Unicode character - || codePoint == 0xfffe || codePoint == 0xffff) - return false; - } - - if (CURRENT_OS == WINDOWS) { // Windows only - char lastChar = name.charAt(name.length() - 1); - // filenames ending in dot are not valid - if (lastChar == '.') - return false; - // file names ending with whitespace are truncated (bug 118997) - if (Character.isWhitespace(lastChar)) - return false; - int dot = name.indexOf('.'); - // on windows, filename suffixes are not relevant to name validity - String basename = dot == -1 ? name : name.substring(0, dot); - if (Arrays.binarySearch(INVALID_RESOURCE_BASENAMES, basename.toLowerCase(Locale.ROOT)) >= 0) - return false; - if (Arrays.binarySearch(INVALID_RESOURCE_FULLNAMES, name.toLowerCase(Locale.ROOT)) >= 0) - return false; - if (INVALID_RESOURCE_CHARACTERS.matcher(name).find()) - return false; - } - - return true; - } - } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/FileUtilsTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/FileUtilsTest.java new file mode 100644 index 000000000..f8d824485 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/io/FileUtilsTest.java @@ -0,0 +1,70 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.io; + +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.*; + +/// @author Glavo +public class FileUtilsTest { + + @ParameterizedTest + @EnumSource(OperatingSystem.class) + public void testIsNameValid(OperatingSystem os) { + assertTrue(FileUtils.isNameValid(os, "example")); + assertTrue(FileUtils.isNameValid(os, "example.zip")); + assertTrue(FileUtils.isNameValid(os, "example.tar.gz")); + assertTrue(FileUtils.isNameValid(os, "\uD83D\uDE00")); + assertTrue(FileUtils.isNameValid(os, "a\uD83D\uDE00b")); + + assertFalse(FileUtils.isNameValid(os, "")); + assertFalse(FileUtils.isNameValid(os, ".")); + assertFalse(FileUtils.isNameValid(os, "..")); + assertFalse(FileUtils.isNameValid(os, "exam\0ple")); + assertFalse(FileUtils.isNameValid(os, "example/0")); + + // Test for invalid surrogate pair + assertFalse(FileUtils.isNameValid(os, "\uD83D")); + assertFalse(FileUtils.isNameValid(os, "\uDE00")); + assertFalse(FileUtils.isNameValid(os, "\uDE00\uD83D")); + assertFalse(FileUtils.isNameValid(os, "\uD83D\uD83D")); + assertFalse(FileUtils.isNameValid(os, "a\uD83D")); + assertFalse(FileUtils.isNameValid(os, "a\uDE00")); + assertFalse(FileUtils.isNameValid(os, "a\uDE00\uD83D")); + assertFalse(FileUtils.isNameValid(os, "a\uD83Db")); + assertFalse(FileUtils.isNameValid(os, "a\uDE00b")); + assertFalse(FileUtils.isNameValid(os, "a\uDE00\uD83Db")); + + // Platform-specific tests + boolean isWindows = os == OperatingSystem.WINDOWS; + boolean isNotWindows = !isWindows; + assertEquals(isNotWindows, FileUtils.isNameValid(os, "com1")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "com1.txt")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "foo.")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "foo ")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "foo")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "f:oo")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "f?oo")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "f*oo")); + assertEquals(isNotWindows, FileUtils.isNameValid(os, "f\\oo")); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/OperatingSystemTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/OperatingSystemTest.java deleted file mode 100644 index 212a07899..000000000 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/OperatingSystemTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.util.platform; - -import org.junit.jupiter.api.Test; - -import static org.jackhuang.hmcl.util.platform.OperatingSystem.isNameValid; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * @author Glavo - */ -public final class OperatingSystemTest { - @Test - public void testIsNameValid() { - assertTrue(isNameValid("example")); - assertTrue(isNameValid("example.zip")); - assertTrue(isNameValid("example.tar.gz")); - assertTrue(isNameValid("\uD83D\uDE00")); - assertTrue(isNameValid("a\uD83D\uDE00b")); - - assertFalse(isNameValid(".")); - assertFalse(isNameValid("..")); - assertFalse(isNameValid("exam\0ple")); - assertFalse(isNameValid("example/0")); - - // Test for invalid surrogate pair - assertFalse(isNameValid("\uD83D")); - assertFalse(isNameValid("\uDE00")); - assertFalse(isNameValid("\uDE00\uD83D")); - assertFalse(isNameValid("\uD83D\uD83D")); - assertFalse(isNameValid("a\uD83D")); - assertFalse(isNameValid("a\uDE00")); - assertFalse(isNameValid("a\uDE00\uD83D")); - assertFalse(isNameValid("a\uD83Db")); - assertFalse(isNameValid("a\uDE00b")); - assertFalse(isNameValid("a\uDE00\uD83Db")); - } -}