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