From 0958d5c51bb5e776d8262f3689abffa8dcf02486 Mon Sep 17 00:00:00 2001 From: Hanabi <317387557+Hanabi9248@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:04:32 +0800 Subject: [PATCH 1/5] fix(common): decode resource file URL paths once Preserve literal plus signs when resolving local resources. Signed-off-by: Hanabi9248 --- common/src/main/java/com/highgo/cloud/util/SshUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/com/highgo/cloud/util/SshUtil.java b/common/src/main/java/com/highgo/cloud/util/SshUtil.java index 08c0807..f40bf75 100644 --- a/common/src/main/java/com/highgo/cloud/util/SshUtil.java +++ b/common/src/main/java/com/highgo/cloud/util/SshUtil.java @@ -631,7 +631,7 @@ public static String getCurrentDirPath() { if (path.contains(".jar!/")) { path = path.substring(0, path.indexOf(".jar!/") + 4); } - File file = new File(path); + File file = new File(java.net.URI.create(path).getPath()); path = file.getParentFile().getAbsolutePath(); return path; } @@ -681,7 +681,7 @@ public static void copyDirFilesFromJar(Class clazz, String dirPath) throws IOExc } } else { // ide运行 - File file = new File(url.getPath()); + File file = new File(java.net.URI.create(url.toString())); List allFileList = new ArrayList<>(); getAllFile(file, allFileList); From 772dac4696547dcdece57a3ab8313225a8944942 Mon Sep 17 00:00:00 2001 From: Hanabi <317387557+Hanabi9248@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:04:48 +0800 Subject: [PATCH 2/5] test(common): add ResourcePathRegression regression Signed-off-by: Hanabi9248 --- tests/regression/ResourcePathRegression.java | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tests/regression/ResourcePathRegression.java diff --git a/tests/regression/ResourcePathRegression.java b/tests/regression/ResourcePathRegression.java new file mode 100644 index 0000000..eb5e81c --- /dev/null +++ b/tests/regression/ResourcePathRegression.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** Real class-loader and filesystem regression for the complete production SshUtil. */ +public final class ResourcePathRegression { + private static int passed; + private static int failed; + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } + + private static void runCase(String name, String suffix, boolean resourceWalk, String[] args) { + try { + Path privateRoot = Paths.get(args[0]).toRealPath(); + Path classes = Paths.get(args[1]).toRealPath(); + Path realSpring = Paths.get(args[2]).toRealPath(); + require(Files.isDirectory(privateRoot) && privateRoot.getParent() != null, + "Main must supply a private non-root directory"); + Path work = Files.createTempDirectory(privateRoot, "ivory-resource-path-").toRealPath(); + require(work.getParent().equals(privateRoot), "Unexpected private fixture boundary"); + Path springHome = Files.createDirectory(work.resolve(suffix)); + Path springFixture = springHome.resolve("spring-core.jar"); + // Copy the existing real dependency byte-for-byte only into this + // runtime fixture; no dependency source or archive is modified. + Files.copy(realSpring, springFixture); + require(Files.size(realSpring) == Files.size(springFixture), "Incomplete dependency copy"); + Path resourceHome = Files.createDirectory(work.resolve("resources-" + suffix)); + Files.createDirectories(resourceHome.resolve("monitor/ivory")); + Files.copy(classes.resolve("ResourcePathMarker.class"), + resourceHome.resolve("ResourcePathMarker.class")); + + List urls = new ArrayList<>(); + urls.add(resourceHome.toUri().toURL()); + urls.add(classes.toUri().toURL()); + urls.add(springFixture.toUri().toURL()); + for (int i = 3; i < args.length; i++) { + urls.add(Paths.get(args[i]).toRealPath().toUri().toURL()); + } + // Do not delegate to the system application's Spring copy: the + // production code must see the real encoded fixture CodeSource. + ClassLoader platform = ClassLoader.getSystemClassLoader().getParent(); + try (URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[0]), platform)) { + Class ssh = Class.forName("com.highgo.cloud.util.SshUtil", true, loader); + if (resourceWalk) { + Class marker = Class.forName("ResourcePathMarker", true, loader); + ssh.getMethod("copyDirFilesFromJar", Class.class, String.class) + .invoke(null, marker, "monitor/ivory"); + // Empty but valid classpath directories exercise the real + // IDE traversal without writing into the dependency cache. + try (Stream entries = Files.list(springHome)) { + require(entries.count() == 1, "Empty traversal unexpectedly copied files"); + } + } else { + String actual = (String) ssh.getMethod("getCurrentDirPath").invoke(null); + require(Paths.get(actual).toAbsolutePath().normalize().equals(springHome), + "CodeSource directory was not decoded exactly once: " + actual + + " expected " + springHome); + } + } + passed++; + System.out.println("PASS " + name); + } catch (Throwable error) { + failed++; + System.err.println("FAIL " + name); + Throwable cause = error instanceof InvocationTargetException + ? ((InvocationTargetException) error).getCause() : error; + cause.printStackTrace(System.err); + } + // Main owns cleanup of its private root after the JVM has released JARs. + } + + public static void main(String[] args) throws Exception { + if (args.length < 4) { + throw new IllegalArgumentException( + "privateRoot classes realSpringCore.jar otherRealDependency.jar ..."); + } + Path root = Paths.get(args[0]).toRealPath(); + require(root.toUri().getRawPath().equals(root.toUri().getPath()), + "Use Main's private ASCII path without spaces/percent escapes so plain controls stay plain"); + runCase("CodeSource directory with spaces", "library space", false, args); + runCase("CodeSource directory with Unicode", "库目录", false, args); + runCase("plain CodeSource control", "plain", false, args); + runCase("literal plus CodeSource control", "literal+plus", false, args); + runCase("classpath directory with spaces", "resource space", true, args); + runCase("plain classpath directory control", "resource-plain", true, args); + System.out.println("RESULT passed=" + passed + " failed=" + failed); + if (failed != 0) { + throw new AssertionError("Regression failures: " + failed); + } + } +} From c3169088154fa67414983845a6c9d5152e984e3d Mon Sep 17 00:00:00 2001 From: Hanabi <317387557+Hanabi9248@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:04:50 +0800 Subject: [PATCH 3/5] test(common): add ResourcePathMarker regression Signed-off-by: Hanabi9248 --- tests/regression/ResourcePathMarker.java | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/regression/ResourcePathMarker.java diff --git a/tests/regression/ResourcePathMarker.java b/tests/regression/ResourcePathMarker.java new file mode 100644 index 0000000..804fd1b --- /dev/null +++ b/tests/regression/ResourcePathMarker.java @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** An application class whose real class loader owns the local resource fixture. */ +public final class ResourcePathMarker { + private ResourcePathMarker() { + } +} From 1c0ce3f6704ebfafcb7dff7f642803f19f597f1b Mon Sep 17 00:00:00 2001 From: Hanabi <317387557+Hanabi9248@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:36:45 +0800 Subject: [PATCH 4/5] fix(common): copy single classpath resources in IDE mode Use the existing copy pipeline for the alert-rule XML caller; preserve directory traversal and JAR behavior. Signed-off-by: Hanabi9248 --- common/src/main/java/com/highgo/cloud/util/SshUtil.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/src/main/java/com/highgo/cloud/util/SshUtil.java b/common/src/main/java/com/highgo/cloud/util/SshUtil.java index f40bf75..3b1e95c 100644 --- a/common/src/main/java/com/highgo/cloud/util/SshUtil.java +++ b/common/src/main/java/com/highgo/cloud/util/SshUtil.java @@ -682,6 +682,10 @@ public static void copyDirFilesFromJar(Class clazz, String dirPath) throws IOExc } else { // ide运行 File file = new File(java.net.URI.create(url.toString())); + if (file.isFile()) { + SshUtil.copy(dirPath); + return; + } List allFileList = new ArrayList<>(); getAllFile(file, allFileList); From 88e49b1f5ddbc171e729715f815c106add767890 Mon Sep 17 00:00:00 2001 From: Hanabi <317387557+Hanabi9248@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:36:59 +0800 Subject: [PATCH 5/5] test(common): exercise single and recursive classpath resource copies Use official alert XML, real Spring resolution and nonempty binary data in private filesystem fixtures. Signed-off-by: Hanabi9248 --- tests/regression/ResourceFileRegression.java | 189 +++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tests/regression/ResourceFileRegression.java diff --git a/tests/regression/ResourceFileRegression.java b/tests/regression/ResourceFileRegression.java new file mode 100644 index 0000000..dfda1a7 --- /dev/null +++ b/tests/regression/ResourceFileRegression.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.File; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** Full SshUtil, real Spring resource resolution, and nonempty private files. */ +public final class ResourceFileRegression { + private static final String XML_PATH = "monitor/ivory/alert-rule.xml"; + private static final String BINARY_PATH = "monitor/ivory/dashboards/chart.bin"; + private static final byte[] BINARY = {0, 1, -1, 10, 13, 42}; + private static int passed; + private static int failed; + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } + + private static void verifyBytes(Path target, byte[] expected) throws Exception { + require(Files.isRegularFile(target), "Missing copied file: " + target); + require(Arrays.equals(expected, Files.readAllBytes(target)), + "Copied bytes differ: " + target); + } + + private static void verifyXml(Path target) throws Exception { + Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() + .parse(target.toFile()); + require("alert-rule".equals(doc.getDocumentElement().getNodeName()), + "Wrong XML root"); + NodeList rules = doc.getElementsByTagName("rule"); + require(rules.getLength() == 4, "Expected four official rules"); + String[] names = {"CPULow", "CPUHigh", "MemoryLow", "MemoryHigh"}; + String[] thresholds = {"30", "80", "30", "80"}; + for (int i = 0; i < names.length; i++) { + Element rule = (Element) rules.item(i); + require(names[i].equals(rule.getAttribute("name")), "Wrong rule name"); + Element threshold = (Element) rule.getElementsByTagName("threshold").item(0); + require(thresholds[i].equals(threshold.getAttribute("value")), + "Wrong threshold"); + } + } + + private static URL resourceUrl(Path resources, Path work, boolean jar) throws Exception { + if (!jar) { + return resources.toUri().toURL(); + } + Path archive = work.resolve("application-resources.jar"); + try (OutputStream output = Files.newOutputStream(archive); + JarOutputStream zip = new JarOutputStream(output); + Stream paths = Files.walk(resources)) { + for (Path file : paths.collect(Collectors.toList())) { + String relative = resources.relativize(file).toString().replace(File.separatorChar, '/'); + String entry = "BOOT-INF/classes/" + relative; + if (Files.isDirectory(file) && !entry.endsWith("/")) { + entry += "/"; + } + zip.putNextEntry(new JarEntry(entry)); + if (Files.isRegularFile(file)) { + Files.copy(file, zip); + } + zip.closeEntry(); + } + } + return new URL("jar:" + archive.toUri().toURL() + "!/BOOT-INF/classes/"); + } + + private static void runCase(String name, String requested, boolean directory, + boolean jar, String[] args) { + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + try { + Path privateRoot = Paths.get(args[1]).toRealPath(); + Path classes = Paths.get(args[2]).toRealPath(); + Path realSpring = Paths.get(args[3]).toRealPath(); + byte[] officialXml = Files.readAllBytes(Paths.get(args[4]).toRealPath()); + require(Files.isDirectory(privateRoot) && privateRoot.getParent() != null, + "Main must supply a private non-root directory"); + Path work = Files.createTempDirectory(privateRoot, "ivory-resource-file-").toRealPath(); + require(work.getParent().equals(privateRoot), "Invalid private fixture boundary"); + Path library = Files.createDirectory(work.resolve("library")); + Path resources = Files.createDirectory(work.resolve("resources")); + Path springCopy = library.resolve("spring-core.jar"); + Files.copy(realSpring, springCopy); + verifyBytes(springCopy, Files.readAllBytes(realSpring)); + Files.copy(classes.resolve("ResourcePathMarker.class"), + resources.resolve("ResourcePathMarker.class")); + Path sourceXml = resources.resolve(XML_PATH); + Path sourceBinary = resources.resolve(BINARY_PATH); + Files.createDirectories(sourceBinary.getParent()); + Files.write(sourceXml, officialXml); + Files.write(sourceBinary, BINARY); + require(!library.startsWith(resources) && !resources.startsWith(library), + "Source and destination must be separate private trees"); + + List urls = new ArrayList<>(); + urls.add(resourceUrl(resources, work, jar)); + urls.add(classes.toUri().toURL()); + urls.add(springCopy.toUri().toURL()); + for (int i = 5; i < args.length; i++) { + urls.add(Paths.get(args[i]).toRealPath().toUri().toURL()); + } + ClassLoader platform = ClassLoader.getSystemClassLoader().getParent(); + try (URLClassLoader loader = new URLClassLoader(urls.toArray(new URL[0]), platform)) { + Thread.currentThread().setContextClassLoader(loader); + Class ssh = Class.forName("com.highgo.cloud.util.SshUtil", true, loader); + Class marker = Class.forName("ResourcePathMarker", true, loader); + require(marker.getClassLoader() == loader, "Fixture marker delegated unexpectedly"); + String destination = (String) ssh.getMethod("getCurrentDirPath").invoke(null); + require(Paths.get(destination).toRealPath().equals(library), + "Copy must target the private Spring CodeSource parent"); + ssh.getMethod("copyDirFilesFromJar", Class.class, String.class) + .invoke(null, marker, requested); + if (directory || XML_PATH.equals(requested)) { + verifyBytes(library.resolve(XML_PATH), officialXml); + verifyXml(library.resolve(XML_PATH)); + } + if (directory || BINARY_PATH.equals(requested)) { + verifyBytes(library.resolve(BINARY_PATH), BINARY); + } + verifyBytes(sourceXml, officialXml); + verifyBytes(sourceBinary, BINARY); + Thread.currentThread().setContextClassLoader(previous); + } + passed++; + System.out.println("PASS " + name); + } catch (Throwable error) { + failed++; + System.err.println("FAIL " + name); + Throwable cause = error instanceof InvocationTargetException + ? ((InvocationTargetException) error).getCause() : error; + cause.printStackTrace(System.err); + } finally { + Thread.currentThread().setContextClassLoader(previous); + } + // Main owns private-root cleanup after all real JAR handles close. + } + + public static void main(String[] args) throws Exception { + if (args.length < 6 || !("ide".equals(args[0]) || "jar".equals(args[0]))) { + throw new IllegalArgumentException( + "ide|jar privateRoot classes realSpring.jar officialAlertXml otherDependency.jar ..."); + } + boolean jar = "jar".equals(args[0]); + if (jar && File.separatorChar != '/') { + throw new UnsupportedOperationException( + "Existing JAR branch compares native Paths separators with BOOT-INF/classes/; " + + "run this control on Unix, do not count it as a Windows pass or skip"); + } + runCase("official single XML resource", XML_PATH, false, jar, args); + runCase("single binary resource", BINARY_PATH, false, jar, args); + runCase("nonempty recursive directory control", "monitor/ivory", true, jar, args); + System.out.println("RESULT passed=" + passed + " failed=" + failed); + if (failed != 0) { + throw new AssertionError("Regression failures: " + failed); + } + } +}