/* * 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()); } } }