jdk-24/test/langtools/tools/javac/classreader/InvalidModifiedUtf8Test.java
2023-03-28 16:14:37 +00:00

253 lines
9.2 KiB
Java

/*
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8303623
* @modules jdk.compiler/com.sun.tools.javac.code
* @summary Compiler should disallow non-standard UTF-8 string encodings
*/
import com.sun.tools.javac.code.Source;
import com.sun.tools.javac.Main;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.util.Arrays;
public class InvalidModifiedUtf8Test {
//
// What this test does (repeatedly):
// 1. Compile a Java source file for ClassX normally
// 2. Modify the UTF-8 inside the ClassX classfile so that it is
// still valid structurally but uses a non-standard way of
// encoding some character (according to "Modified UTF-8").
// 3. Compile a Java source file for RefClass that references ClassX
// 4. Verify that the compiler gives a "bad UTF-8" error
//
// We change c3 a8 -> c3 e8 (illegal second byte not of the form 0x10xxxxxx)
private static final String SOURCE_0 = """
interface CLASSNAME {
void ABC\u00e8(); // encodes to: 41 42 43 c3 a8
}
""";
// We change e1 80 80 -> e1 80 40 (illegal third byte not of the form 0x10xxxxxx)
private static final String SOURCE_1 = """
interface CLASSNAME {
void ABC\u1000(); // encodes to: 41 42 43 e1 80 80
}
""";
// We change c4 80 -> c1 81 (illegal two-byte encoding for one-byte value)
private static final String SOURCE_2 = """
interface CLASSNAME {
void ABC\u0100(); // encodes to: 41 42 43 c4 00
}
""";
// We change e1 80 80 -> e0 84 80 (illegal three-byte encoding for two-byte value)
private static final String SOURCE_3 = """
interface CLASSNAME {
void ABC\u1000(); // encodes to: 41 42 43 e1 80 80
}
""";
// We change 44 -> 00 (illegal one-byte encoding of 0x0000)
private static final String SOURCE_4 = """
interface CLASSNAME {
void ABCD(); // encodes to: 41 42 43 44
}
""";
// We change 43 44 -> e1 80 (illegal truncated three-byte encoding)
private static final String SOURCE_5 = """
interface CLASSNAME {
void ABCD(); // encodes to: 41 42 43 44
}
""";
// This is the source file that references one of the above
private static final String REF_SOURCE = """
interface RefClass extends CLASSNAME {
}
""";
private static TestCase[] TEST_CASES = new TestCase[] {
new TestCase(0, SOURCE_0, "414243c3a8", "414243c3e8"),
new TestCase(1, SOURCE_1, "414243e18080", "414243e18040"),
new TestCase(2, SOURCE_2, "414243c480", "414243c181"),
new TestCase(3, SOURCE_3, "414243e18080", "414243e08480"),
new TestCase(4, SOURCE_4, "41424344", "41424300"),
new TestCase(5, SOURCE_5, "41424344", "4142e180"),
};
public static String bytes2string(byte[] array) {
char[] buf = new char[array.length * 2];
for (int i = 0; i < array.length; i++) {
int value = array[i] & 0xff;
buf[i * 2] = Character.forDigit(value >> 4, 16);
buf[i * 2 + 1] = Character.forDigit(value & 0xf, 16);
}
return new String(buf);
}
public static byte[] string2bytes(String string) {
byte[] buf = new byte[string.length() / 2];
for (int i = 0; i < string.length(); i += 2) {
int value = Integer.parseInt(string.substring(i, i + 2), 16);
buf[i / 2] = (byte)value;
}
return buf;
}
private static void createSourceFile(String content, File file) throws IOException {
System.err.println("creating: " + file);
try (PrintStream output = new PrintStream(new FileOutputStream(file))) {
output.println(content);
}
}
private static void writeFile(File file, byte[] content) throws IOException {
System.err.println("writing: " + file);
try (FileOutputStream output = new FileOutputStream(file)) {
Files.write(file.toPath(), content);
}
}
private static void compileRefClass(File file, boolean expectSuccess, String expectedError) {
final StringWriter diags = new StringWriter();
final String[] params = new String[] {
"-classpath",
".",
"-XDrawDiagnostics",
file.toString()
};
System.err.println("compiling: " + file);
int ret = Main.compile(params, new PrintWriter(diags, true));
System.err.println("exit value: " + ret);
String output = diags.toString().trim();
if (!output.isEmpty())
System.err.println("output:\n" + output);
else
System.err.println("no output");
if (!expectSuccess && ret == 0)
throw new AssertionError("compilation succeeded, but expected failure");
else if (expectSuccess && ret != 0)
throw new AssertionError("compilation failed, but expected success");
if (expectedError != null && !diags.toString().contains(expectedError))
throw new AssertionError("expected output \"" + expectedError + "\" not found");
}
public static void main(String... args) throws Exception {
// Create source files
for (TestCase test : TEST_CASES)
test.createSourceFile();
// Compile source files
for (TestCase test : TEST_CASES) {
int ret = Main.compile(new String[] { test.sourceFile().toString() });
if (ret != 0)
throw new AssertionError("compilation of " + test.sourceFile() + " failed");
}
// We should get warnings in JDK 21 and errors in any later release
final boolean expectSuccess = Source.DEFAULT.compareTo(Source.JDK21) <= 0;
// Now compile REF_SOURCE against each classfile without and then with the modification.
// When compiling without the modification, everything should be normal.
// When compiling with the modification, an error should be generated.
for (TestCase test : TEST_CASES) {
System.err.println("==== TEST " + test.index() + " ====");
// Create reference source file
final File refSource = new File("RefClass.java");
createSourceFile(REF_SOURCE.replaceAll("CLASSNAME", test.className()), refSource);
// Do a normal compilation
compileRefClass(refSource, true, null);
// Now corrupt the class file
System.err.println("modifying: " + test.classFile());
final File classFile = test.classFile();
final byte[] data1 = Files.readAllBytes(classFile.toPath());
final byte[] data2 = test.modify(data1);
writeFile(classFile, data2);
// Do a corrupt compilation
compileRefClass(refSource, expectSuccess, "compiler.misc.bad.utf8.byte.sequence.at");
}
}
// TestCase
static class TestCase {
final int index;
final String source;
final String match;
final String replace;
TestCase(int index, String source, String match, String replace) {
this.index = index;
this.source = source.replaceAll("CLASSNAME", className());
this.match = match;
this.replace = replace;
}
byte[] modify(byte[] input) {
final byte[] output = string2bytes(bytes2string(input).replaceAll(match, replace));
if (Arrays.equals(output, input))
throw new AssertionError("modification of " + classFile() + " failed");
return output;
}
int index() {
return index;
}
String className() {
return "Class" + index;
}
File sourceFile() {
return new File(className() + ".java");
}
File classFile() {
return new File(className() + ".class");
}
void createSourceFile() throws IOException {
InvalidModifiedUtf8Test.createSourceFile(source, sourceFile());
}
}
}