From e49f65e7a69316722ee5c51b5b827fbcd894be0e Mon Sep 17 00:00:00 2001 From: rootkiller6788 Date: Thu, 20 Aug 2026 13:10:45 +0800 Subject: [PATCH 1/2] Encode location names in InMemoryJavaFileManager file URIs Module locations returned by StandardJavaFileManager.getLocationForModule have names such as CLASS_OUTPUT[foo]. The brackets are illegal in a URI path, so URI.create("mem:///CLASS_OUTPUT[foo]/...") throws IllegalArgumentException. Percent-encode the brackets before building in-memory file URIs. Fixes #335. --- .../compile/InMemoryJavaFileManager.java | 15 +++- .../compile/InMemoryJavaFileManagerTest.java | 79 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java diff --git a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java index b2641b3a..713af998 100644 --- a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java +++ b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java @@ -69,7 +69,7 @@ public JavaFileObject load(URI key) { } private static URI uriForFileObject(Location location, String packageName, String relativeName) { - StringBuilder uri = new StringBuilder("mem:///").append(location.getName()).append('/'); + StringBuilder uri = new StringBuilder("mem:///").append(locationName(location)).append('/'); if (!packageName.isEmpty()) { uri.append(packageName.replace('.', '/')).append('/'); } @@ -79,7 +79,18 @@ private static URI uriForFileObject(Location location, String packageName, Strin private static URI uriForJavaFileObject(Location location, String className, Kind kind) { return URI.create( - "mem:///" + location.getName() + '/' + className.replace('.', '/') + kind.extension); + "mem:///" + locationName(location) + '/' + className.replace('.', '/') + kind.extension); + } + + /** + * Returns the name of {@code location}, with any characters that are illegal in a URI path + * percent-encoded. Module locations returned by {@code + * StandardJavaFileManager#getLocationForModule} have names such as {@code "CLASS_OUTPUT[foo]"}, + * and the brackets would otherwise make {@link URI#create(String)} throw an {@link + * IllegalArgumentException}. + */ + private static String locationName(Location location) { + return location.getName().replace("[", "%5B").replace("]", "%5D"); } @Override diff --git a/src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java b/src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java new file mode 100644 index 00000000..1af34eca --- /dev/null +++ b/src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2026 Google, Inc. + * + * Licensed 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. + */ +package com.google.testing.compile; + +import static com.google.common.truth.Truth.assertThat; + +import java.nio.charset.StandardCharsets; +import javax.tools.FileObject; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link InMemoryJavaFileManager}. */ +@RunWith(JUnit4.class) +public final class InMemoryJavaFileManagerTest { + + @Test + public void getJavaFileForOutput_moduleLocationName() throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager standardFileManager = + compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8); + InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(standardFileManager)) { + // Module locations returned by StandardJavaFileManager.getLocationForModule() have brackets + // in their name (e.g. "CLASS_OUTPUT[foo]"), which are illegal characters in a URI path. This + // used to throw IllegalArgumentException. + JavaFileObject output = + fileManager.getJavaFileForOutput(moduleLocation(), "com.example.Foo", Kind.CLASS, null); + assertThat(output).isNotNull(); + } + } + + @Test + public void getFileForOutput_moduleLocationName() throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager standardFileManager = + compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8); + InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(standardFileManager)) { + // Module locations returned by StandardJavaFileManager.getLocationForModule() have brackets + // in their name (e.g. "CLASS_OUTPUT[foo]"), which are illegal characters in a URI path. This + // used to throw IllegalArgumentException. + FileObject output = + fileManager.getFileForOutput(moduleLocation(), "com.example", "Foo.txt", null); + assertThat(output).isNotNull(); + } + } + + private static Location moduleLocation() { + return new Location() { + @Override + public String getName() { + return "CLASS_OUTPUT[foo]"; + } + + @Override + public boolean isOutputLocation() { + return true; + } + }; + } +} From d5e1dd4a5e486e608639aba4d153e7dfc83f44da Mon Sep 17 00:00:00 2001 From: rootkiller6788 Date: Sat, 22 Aug 2026 19:10:55 +0800 Subject: [PATCH 2/2] Use the multi-argument URI constructor instead of manual escaping Per review feedback, URI.create() combined with manual percent-encoding is brittle. Build the path from the raw location name and let the URI(scheme, authority, path, fragment) constructor percent-encode any characters that are illegal in a URI path. An empty (rather than null) authority keeps the resulting mem:///... URIs in the same form as before. Also strengthen InMemoryJavaFileManagerTest to assert the encoded URI path and cover getJavaFileForInput(), the path javac's module validation hits. --- .../compile/InMemoryJavaFileManager.java | 33 ++++++++----- .../compile/InMemoryJavaFileManagerTest.java | 48 +++++++++++++++---- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java index 713af998..887b7774 100644 --- a/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java +++ b/src/main/java/com/google/testing/compile/InMemoryJavaFileManager.java @@ -33,6 +33,7 @@ import java.io.StringWriter; import java.io.Writer; import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; @@ -69,28 +70,34 @@ public JavaFileObject load(URI key) { } private static URI uriForFileObject(Location location, String packageName, String relativeName) { - StringBuilder uri = new StringBuilder("mem:///").append(locationName(location)).append('/'); + String path = "/" + location.getName() + "/"; if (!packageName.isEmpty()) { - uri.append(packageName.replace('.', '/')).append('/'); + path += packageName.replace('.', '/') + "/"; } - uri.append(relativeName); - return URI.create(uri.toString()); + path += relativeName; + return memUri(path); } private static URI uriForJavaFileObject(Location location, String className, Kind kind) { - return URI.create( - "mem:///" + locationName(location) + '/' + className.replace('.', '/') + kind.extension); + String path = "/" + location.getName() + "/" + className.replace('.', '/') + kind.extension; + return memUri(path); } /** - * Returns the name of {@code location}, with any characters that are illegal in a URI path - * percent-encoded. Module locations returned by {@code - * StandardJavaFileManager#getLocationForModule} have names such as {@code "CLASS_OUTPUT[foo]"}, - * and the brackets would otherwise make {@link URI#create(String)} throw an {@link - * IllegalArgumentException}. + * Returns an in-memory {@code mem:///...} URI for the given path. The multi-argument {@link URI} + * constructor percent-encodes any characters that are illegal in a URI path, unlike {@link + * URI#create(String)}, which throws {@link IllegalArgumentException}. This matters for module + * locations returned by {@code StandardJavaFileManager#getLocationForModule}, whose names contain + * brackets (e.g. {@code "CLASS_OUTPUT[foo]"}). The empty (rather than null) authority keeps the + * URIs in the {@code "mem:///..."} form they had before. Since the constructor quotes illegal + * characters rather than rejecting them, it cannot fail for the paths built here. */ - private static String locationName(Location location) { - return location.getName().replace("[", "%5B").replace("]", "%5D"); + private static URI memUri(String path) { + try { + return new URI("mem", "", path, null); + } catch (URISyntaxException impossible) { + throw new AssertionError(impossible); + } } @Override diff --git a/src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java b/src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java index 1af34eca..1c7c4d9e 100644 --- a/src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java +++ b/src/test/java/com/google/testing/compile/InMemoryJavaFileManagerTest.java @@ -24,6 +24,7 @@ import javax.tools.JavaFileObject; import javax.tools.JavaFileObject.Kind; import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; import javax.tools.ToolProvider; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,18 +34,34 @@ @RunWith(JUnit4.class) public final class InMemoryJavaFileManagerTest { + @Test + public void getJavaFileForOutput_standardLocation() throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager standardFileManager = + compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8); + InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(standardFileManager)) { + // Standard (non-module) location names contain no characters that need escaping, so the URI + // keeps its previous form. + JavaFileObject output = + fileManager.getJavaFileForOutput( + StandardLocation.CLASS_OUTPUT, "com.example.Foo", Kind.CLASS, null); + assertThat(output.toUri().getRawPath()).isEqualTo("/CLASS_OUTPUT/com/example/Foo.class"); + } + } + @Test public void getJavaFileForOutput_moduleLocationName() throws Exception { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); try (StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8); InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(standardFileManager)) { - // Module locations returned by StandardJavaFileManager.getLocationForModule() have brackets - // in their name (e.g. "CLASS_OUTPUT[foo]"), which are illegal characters in a URI path. This - // used to throw IllegalArgumentException. + // Module locations returned by StandardJavaFileManager.getLocationForModule() have names with + // brackets (e.g. "CLASS_OUTPUT[foo]"), which are illegal characters in a URI path. Building + // the output file's URI used to throw IllegalArgumentException. JavaFileObject output = fileManager.getJavaFileForOutput(moduleLocation(), "com.example.Foo", Kind.CLASS, null); - assertThat(output).isNotNull(); + assertThat(output.toUri().getRawPath()) + .isEqualTo("/CLASS_OUTPUT%5Bfoo%5D/com/example/Foo.class"); } } @@ -54,12 +71,27 @@ public void getFileForOutput_moduleLocationName() throws Exception { try (StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8); InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(standardFileManager)) { - // Module locations returned by StandardJavaFileManager.getLocationForModule() have brackets - // in their name (e.g. "CLASS_OUTPUT[foo]"), which are illegal characters in a URI path. This - // used to throw IllegalArgumentException. + // Module locations returned by StandardJavaFileManager.getLocationForModule() have names with + // brackets (e.g. "CLASS_OUTPUT[foo]"), which are illegal characters in a URI path. Building + // the output file's URI used to throw IllegalArgumentException. FileObject output = fileManager.getFileForOutput(moduleLocation(), "com.example", "Foo.txt", null); - assertThat(output).isNotNull(); + assertThat(output.toUri().getRawPath()) + .isEqualTo("/CLASS_OUTPUT%5Bfoo%5D/com/example/Foo.txt"); + } + } + + @Test + public void getJavaFileForInput_moduleLocationName() throws Exception { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + try (StandardJavaFileManager standardFileManager = + compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8); + InMemoryJavaFileManager fileManager = new InMemoryJavaFileManager(standardFileManager)) { + // javac's module validation calls getJavaFileForInput() on the module's CLASS_OUTPUT location + // (see https://github.com/google/compile-testing/issues/335); building the input URI used to + // throw IllegalArgumentException. + assertThat(fileManager.getJavaFileForInput(moduleLocation(), "com.example.Foo", Kind.CLASS)) + .isNull(); } }