diff --git a/compiler/backend/src/org/jetbrains/jet/codegen/ImplementationBodyCodegen.java b/compiler/backend/src/org/jetbrains/jet/codegen/ImplementationBodyCodegen.java index 0f2f919c1ec..7281a619e98 100644 --- a/compiler/backend/src/org/jetbrains/jet/codegen/ImplementationBodyCodegen.java +++ b/compiler/backend/src/org/jetbrains/jet/codegen/ImplementationBodyCodegen.java @@ -43,6 +43,7 @@ import org.jetbrains.jet.codegen.state.JetTypeMapper; import org.jetbrains.jet.codegen.state.JetTypeMapperMode; import org.jetbrains.jet.descriptors.serialization.ClassData; import org.jetbrains.jet.descriptors.serialization.DescriptorSerializer; +import org.jetbrains.jet.descriptors.serialization.JavaProtoBufUtil; import org.jetbrains.jet.descriptors.serialization.ProtoBuf; import org.jetbrains.jet.lang.descriptors.*; import org.jetbrains.jet.lang.descriptors.impl.MutableClassDescriptor; @@ -227,7 +228,11 @@ public class ImplementationBodyCodegen extends ClassBodyCodegen { ClassData data = new ClassData(createNameResolver(serializer.getNameTable()), classProto); AnnotationVisitor av = v.getVisitor().visitAnnotation(JvmStdlibNames.KOTLIN_INFO_CLASS.getDescriptor(), true); - av.visit("data", data.toBytes()); + AnnotationVisitor array = av.visitArray("data"); + for (String string : JavaProtoBufUtil.encodeBytes(data.toBytes())) { + array.visit(null, string); + } + array.visitEnd(); av.visitEnd(); } diff --git a/compiler/backend/src/org/jetbrains/jet/codegen/NamespaceCodegen.java b/compiler/backend/src/org/jetbrains/jet/codegen/NamespaceCodegen.java index da8cb5a43a7..3742e06a0b6 100644 --- a/compiler/backend/src/org/jetbrains/jet/codegen/NamespaceCodegen.java +++ b/compiler/backend/src/org/jetbrains/jet/codegen/NamespaceCodegen.java @@ -31,6 +31,7 @@ import org.jetbrains.jet.codegen.context.CodegenContext; import org.jetbrains.jet.codegen.context.FieldOwnerContext; import org.jetbrains.jet.codegen.state.GenerationState; import org.jetbrains.jet.descriptors.serialization.DescriptorSerializer; +import org.jetbrains.jet.descriptors.serialization.JavaProtoBufUtil; import org.jetbrains.jet.descriptors.serialization.PackageData; import org.jetbrains.jet.descriptors.serialization.ProtoBuf; import org.jetbrains.jet.lang.descriptors.*; @@ -161,7 +162,11 @@ public class NamespaceCodegen extends MemberCodegen { PackageData data = new PackageData(createNameResolver(serializer.getNameTable()), packageProto.build()); AnnotationVisitor av = v.getClassBuilder().newAnnotation(JvmStdlibNames.KOTLIN_INFO_CLASS.getDescriptor(), true); - av.visit("data", data.toBytes()); + AnnotationVisitor array = av.visitArray("data"); + for (String string : JavaProtoBufUtil.encodeBytes(data.toBytes())) { + array.visit(null, string); + } + array.visitEnd(); av.visitEnd(); } diff --git a/compiler/frontend.java/serialization.java/src/org/jetbrains/jet/descriptors/serialization/JavaProtoBufUtil.java b/compiler/frontend.java/serialization.java/src/org/jetbrains/jet/descriptors/serialization/JavaProtoBufUtil.java index 4b47d32129e..f4791650164 100644 --- a/compiler/frontend.java/serialization.java/src/org/jetbrains/jet/descriptors/serialization/JavaProtoBufUtil.java +++ b/compiler/frontend.java/serialization.java/src/org/jetbrains/jet/descriptors/serialization/JavaProtoBufUtil.java @@ -43,12 +43,71 @@ public class JavaProtoBufUtil { } @NotNull - public static ClassData readClassDataFrom(@NotNull byte[] data) { - return ClassData.read(data, getExtensionRegistry()); + public static ClassData readClassDataFrom(@NotNull String[] data) { + return ClassData.read(decodeBytes(data), getExtensionRegistry()); } @NotNull - public static PackageData readPackageDataFrom(@NotNull byte[] data) { - return PackageData.read(data, getExtensionRegistry()); + public static PackageData readPackageDataFrom(@NotNull String[] data) { + return PackageData.read(decodeBytes(data), getExtensionRegistry()); + } + + /** + * Converts a byte array of serialized data to an array of {@code String} satisfying JVM annotation value argument restrictions: + *
    + *
  1. Each string's length should be no more than 65535
  2. + *
  3. UTF-8 representation of each string cannot contain byte 0x0 or bytes in the range 0xf0..0xff
  4. + *
+ */ + @NotNull + public static String[] encodeBytes(@NotNull byte[] data) { + // Each byte of data is split into two 4-bit parts ('lo' and 'hi'), then lo + 1 and hi + 1 are appended to the string. Hence, each + // byte of the string is in the range 0x01..0x10 and this guarantees there's no byte 0x0 and no bytes in the range 0xf0..0xff + // TODO: use Scala's approach instead (break data into chunks of 7 bits) + int m = 32766; + assert 2 * m <= 65535 : m; + + int n = data.length; + String[] result = new String[(n + m - 1) / m]; + for (int offset = 0, resultIndex = 0; offset < n; offset += m, resultIndex++) { + int length = Math.min(n - offset, m); + byte[] a = new byte[length * 2]; + for (int i = 0; i < length; i++) { + int lo = data[offset + i] & 0x0f; + int hi = (data[offset + i] & 0xf0) >>> 4; + a[2 * i] = (byte) (lo + 1); + a[2 * i + 1] = (byte) (hi + 1); + } + result[resultIndex] = new String(a); + } + return result; + } + + /** + * Converts encoded array of {@code String} obtained by {@link JavaProtoBufUtil#encodeBytes(byte[])} back to a byte array. + */ + @NotNull + public static byte[] decodeBytes(@NotNull String[] data) { + int length = 0; + for (String s : data) { + assert s.length() % 2 == 0 : s.length(); + length += s.length() / 2; + } + byte[] result = new byte[length]; + + int p = 0; + for (String s : data) { + for (int i = 0, n = s.length(); i < n; i += 2) { + int lo = s.charAt(i) - 1; + int hi = s.charAt(i + 1) - 1; + assert 0 <= lo && lo < 0xf0 : lo; + assert 0 <= hi && hi < 0xf0 : hi; + result[p++] = (byte) (lo + (hi << 4)); + } + } + + assert p == length; + + return result; } } diff --git a/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/java/resolver/DeserializedDescriptorResolver.java b/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/java/resolver/DeserializedDescriptorResolver.java index e723a284255..8e7b4115a34 100644 --- a/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/java/resolver/DeserializedDescriptorResolver.java +++ b/compiler/frontend.java/src/org/jetbrains/jet/lang/resolve/java/resolver/DeserializedDescriptorResolver.java @@ -40,6 +40,8 @@ import org.jetbrains.jet.utils.ExceptionUtils; import javax.inject.Inject; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import static org.jetbrains.asm4.ClassReader.*; import static org.jetbrains.jet.lang.resolve.java.DescriptorSearchRule.INCLUDE_KOTLIN; @@ -136,25 +138,25 @@ public final class DeserializedDescriptorResolver { @Nullable private static ClassData readClassDataFromClassFile(@NotNull VirtualFile virtualFile) { - byte[] data = getKotlinInfoDataFromClassFile(virtualFile); + String[] data = getKotlinInfoDataFromClassFile(virtualFile); if (data == null) return null; return JavaProtoBufUtil.readClassDataFrom(data); } @Nullable private static PackageData readPackageDataFromClassFile(@NotNull VirtualFile virtualFile) { - byte[] data = getKotlinInfoDataFromClassFile(virtualFile); + String[] data = getKotlinInfoDataFromClassFile(virtualFile); if (data == null) return null; return JavaProtoBufUtil.readPackageDataFrom(data); } @Nullable - private static byte[] getKotlinInfoDataFromClassFile(@NotNull VirtualFile virtualFile) { + private static String[] getKotlinInfoDataFromClassFile(@NotNull VirtualFile virtualFile) { GetKotlinInfoDataVisitor visitor = visitClassFile(virtualFile); if (visitor == null) { return null; } - byte[] data = visitor.getData(); + String[] data = visitor.getData(); if (data == null) { return null; } @@ -186,28 +188,43 @@ public final class DeserializedDescriptorResolver { } @Nullable - private byte[] data = null; + private String[] data = null; @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (!desc.equals(KOTLIN_INFO_TYPE)) { return null; } + return new AnnotationVisitor(Opcodes.ASM4) { @Override - public void visit(String name, Object value) { - if (name.equals("data")) { - data = (byte[]) value; - } - else { - throw new IllegalStateException("Unexpected property " + name + " for annotation " + KOTLIN_INFO_TYPE); + public AnnotationVisitor visitArray(String name) { + if (!name.equals("data")) { + throw new IllegalStateException("Unexpected argument " + name + " for annotation " + KOTLIN_INFO_TYPE); } + + final List strings = new ArrayList(1); + return new AnnotationVisitor(Opcodes.ASM4) { + @Override + public void visit(String name, Object value) { + if (!(value instanceof String)) { + throw new IllegalStateException("Unexpected argument value: " + value); + } + + strings.add((String) value); + } + + @Override + public void visitEnd() { + data = strings.toArray(new String[strings.size()]); + } + }; } }; } @Nullable - public byte[] getData() { + public String[] getData() { return data; } } diff --git a/compiler/tests/compiler-tests.iml b/compiler/tests/compiler-tests.iml index 2e820b9b71b..d275ff90aa4 100644 --- a/compiler/tests/compiler-tests.iml +++ b/compiler/tests/compiler-tests.iml @@ -17,6 +17,7 @@ + diff --git a/compiler/tests/org/jetbrains/jet/codegen/KotlinInfoForPackageTest.java b/compiler/tests/org/jetbrains/jet/codegen/KotlinInfoForPackageTest.java index 4d517ceafc8..0d2ae84d778 100644 --- a/compiler/tests/org/jetbrains/jet/codegen/KotlinInfoForPackageTest.java +++ b/compiler/tests/org/jetbrains/jet/codegen/KotlinInfoForPackageTest.java @@ -19,6 +19,7 @@ package org.jetbrains.jet.codegen; import jet.KotlinInfo; import org.jetbrains.annotations.NotNull; import org.jetbrains.jet.ConfigurationKind; +import org.jetbrains.jet.descriptors.serialization.JavaProtoBufUtil; import org.jetbrains.jet.descriptors.serialization.PackageData; import org.jetbrains.jet.descriptors.serialization.ProtoBuf; import org.jetbrains.jet.lang.resolve.java.PackageClassUtils; @@ -29,8 +30,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import static com.google.protobuf.ExtensionRegistryLite.getEmptyRegistry; - public class KotlinInfoForPackageTest extends CodegenTestCase { public static final FqName NAMESPACE_NAME = new FqName("test"); @@ -54,7 +53,7 @@ public class KotlinInfoForPackageTest extends CodegenTestCase { assertTrue(aClass.isAnnotationPresent(KotlinInfo.class)); KotlinInfo kotlinInfo = (KotlinInfo) aClass.getAnnotation(KotlinInfo.class); - PackageData data = PackageData.read(kotlinInfo.data(), getEmptyRegistry()); + PackageData data = JavaProtoBufUtil.readPackageDataFrom(kotlinInfo.data()); Set classNames = collectClassNames(data); assertSameElements(Arrays.asList("A", "B", "C"), classNames); diff --git a/compiler/tests/org/jetbrains/jet/descriptors/serialization/JavaProtoBufUtilTest.java b/compiler/tests/org/jetbrains/jet/descriptors/serialization/JavaProtoBufUtilTest.java new file mode 100644 index 00000000000..b611c413c0d --- /dev/null +++ b/compiler/tests/org/jetbrains/jet/descriptors/serialization/JavaProtoBufUtilTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2013 JetBrains s.r.o. + * + * 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 org.jetbrains.jet.descriptors.serialization; + +import com.intellij.testFramework.UsefulTestCase; + +import java.util.Random; + +import static org.jetbrains.jet.descriptors.serialization.JavaProtoBufUtil.decodeBytes; +import static org.jetbrains.jet.descriptors.serialization.JavaProtoBufUtil.encodeBytes; +import static org.junit.Assert.assertArrayEquals; + +public class JavaProtoBufUtilTest extends UsefulTestCase { + private static final int[] LENGTHS = new int[] {0, 1, 2, 3, 7, 10, 100, 1000, 32000, 33000, 65000, 65534, 65535, 65536, 65537, + 100000, 131074, 239017, 314159, 1000000}; + + private static void doTest(int randSeed, int length) { + byte[] a = new byte[length]; + new Random(randSeed).nextBytes(a); + + String message = "Failed randSeed = " + randSeed + ", length = " + length; + + String[] b = encodeBytes(a); + byte[] c = decodeBytes(b); + assertArrayEquals(message, a, c); + + String[] d = encodeBytes(c); + assertArrayEquals(message, d, b); + + byte[] e = decodeBytes(d); + assertArrayEquals(message, a, e); + } + + public void testEncodeDecode() { + for (int randSeed = 1; randSeed <= 3; randSeed++) { + for (int length : LENGTHS) { + doTest(randSeed, length); + } + } + } +} diff --git a/compiler/tests/org/jetbrains/jet/descriptors/serialization/KotlinInfoForClassTest.java b/compiler/tests/org/jetbrains/jet/descriptors/serialization/KotlinInfoForClassTest.java index 844f4cc9878..cd1942c8843 100644 --- a/compiler/tests/org/jetbrains/jet/descriptors/serialization/KotlinInfoForClassTest.java +++ b/compiler/tests/org/jetbrains/jet/descriptors/serialization/KotlinInfoForClassTest.java @@ -32,8 +32,6 @@ import org.jetbrains.jet.lang.resolve.name.FqNameUnsafe; import java.io.IOException; -import static com.google.protobuf.ExtensionRegistryLite.getEmptyRegistry; - public class KotlinInfoForClassTest extends CodegenTestCase { public static final FqName NAMESPACE_NAME = new FqName("test"); public static final FqNameUnsafe CLASS_NAME = new FqNameUnsafe("A"); @@ -41,7 +39,7 @@ public class KotlinInfoForClassTest extends CodegenTestCase { @Override protected void setUp() throws Exception { super.setUp(); - createEnvironmentWithMockJdkAndIdeaAnnotations(ConfigurationKind.ALL); + createEnvironmentWithMockJdkAndIdeaAnnotations(ConfigurationKind.JDK_ONLY); } public void testClassKotlinInfo() throws Exception { @@ -70,7 +68,7 @@ public class KotlinInfoForClassTest extends CodegenTestCase { public KotlinInfoBasedDescriptorFinder(@NotNull KotlinInfo kotlinInfo) throws IOException { super(new LockBasedStorageManager(), AnnotationDeserializer.UNSUPPORTED); - this.classData = ClassData.read(kotlinInfo.data(), getEmptyRegistry()); + this.classData = JavaProtoBufUtil.readClassDataFrom(kotlinInfo.data()); this.namespace = JetTestUtils.createTestNamespace(NAMESPACE_NAME.shortName()); } diff --git a/runtime/src/jet/KotlinInfo.java b/runtime/src/jet/KotlinInfo.java index 3319dcdeb69..8e9398a2f42 100644 --- a/runtime/src/jet/KotlinInfo.java +++ b/runtime/src/jet/KotlinInfo.java @@ -21,5 +21,5 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface KotlinInfo { - byte[] data(); + String[] data(); }