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 f89493d1f..ecb098510 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 @@ -306,12 +306,39 @@ public enum OperatingSystem { if (name.isEmpty()) return false; // . and .. have special meaning on all platforms - if (name.equals(".")) - return false; - // \0 and / are forbidden on all platforms - if (name.indexOf('/') != -1 || name.indexOf('\0') != -1) + 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 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 new file mode 100644 index 000000000..212a07899 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/OperatingSystemTest.java @@ -0,0 +1,55 @@ +/* + * 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")); + } +}