Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 2 commits
  • 4 files changed
  • 0 comments
  • 1 contributor
Apr 12, 2012
Robey Pointer better docs 695c781
Robey Pointer and now signature matching 6277561
51 src/main/scala/com/twitter/deconstruct/ClassSignature.scala
... ... @@ -0,0 +1,51 @@
  1 +package com.twitter.deconstruct
  2 +
  3 +case class FieldSignature(name: String, descriptor: String, flags: Int) {
  4 + def signature = "%s(%08x):%s".format(name, flags, descriptor)
  5 +
  6 + // must be an exact match.
  7 + def isCompatibleWith(old: FieldSignature): Boolean = {
  8 + name == old.name && descriptor == old.descriptor && flags == old.flags
  9 + }
  10 +}
  11 +
  12 +case class ClassSignature(
  13 + name: String,
  14 + flags: Int,
  15 + superclass: String,
  16 + interfaces: Seq[String],
  17 + fields: Seq[FieldSignature],
  18 + methods: Seq[FieldSignature]
  19 +) {
  20 + def signature = {
  21 + "%s(%08x):%s:%s\n".format(name, flags, superclass, interfaces.mkString("{", ":", "}")) +
  22 + fields.map { f => "F:%s\n".format(f.signature) }.mkString +
  23 + methods.map { m => "M:%s\n".format(m.signature) }.mkString
  24 + }
  25 +
  26 + def isCompatibleWith(old: ClassSignature): Boolean = {
  27 + name == old.name &&
  28 + flags == old.flags &&
  29 + superclass == old.superclass &&
  30 + interfaces == old.interfaces &&
  31 + old.fields.forall { of => fields.exists { f => f isCompatibleWith of } } &&
  32 + old.methods.forall { om => methods.exists { m => m isCompatibleWith om } }
  33 + }
  34 +}
  35 +
  36 +object ClassSignature {
  37 + /**
  38 + * Create a class signature out of a parsed java class file.
  39 + * The signature can be used to check compatibility with previous versions of the same class.
  40 + */
  41 + def apply(classfile: ClassFile): ClassSignature = {
  42 + ClassSignature(
  43 + classfile.className,
  44 + classfile.accessFlags,
  45 + classfile.superclassName,
  46 + classfile.interfaces,
  47 + classfile.fields.map { f => FieldSignature(f.name, f.descriptor, f.accessFlags) },
  48 + classfile.methods.map { m => FieldSignature(m.name, m.descriptor, m.accessFlags) }
  49 + )
  50 + }
  51 +}
61 src/main/scala/com/twitter/deconstruct/Deconstruct.scala
@@ -6,8 +6,8 @@ import scala.collection.mutable
6 6
7 7 class ParseException(reason: String) extends Exception(reason)
8 8
9   -sealed trait ConstantPoolEntry
10   -object ConstantPoolEntry {
  9 +private[deconstruct] sealed trait ConstantPoolEntry
  10 +private[deconstruct] object ConstantPoolEntry {
11 11 case class Utf8(dataIndex: Int, length: Int) extends ConstantPoolEntry
12 12 case class I32(n: Int) extends ConstantPoolEntry
13 13 case class F32(n: Float) extends ConstantPoolEntry
@@ -21,7 +21,11 @@ object ConstantPoolEntry {
21 21 case class NameAndType(nameIndex: Int, typeIndex: Int) extends ConstantPoolEntry
22 22 }
23 23
24   -class ConstantPool(count: Int, data: ByteBuffer) {
  24 +/**
  25 + * In java class files, the "constant pool" is where almost all string data is stored. Most other
  26 + * data types are indirect pointers into this pool.
  27 + */
  28 +private[deconstruct] class ConstantPool(count: Int, data: ByteBuffer) {
25 29 import ConstantPoolEntry._
26 30
27 31 private[this] val pool = new Array[ConstantPoolEntry](count)
@@ -80,23 +84,26 @@ case class ClassFile(
80 84 methods: Seq[Deconstruct.Field],
81 85 attributes: Seq[Deconstruct.Attribute]
82 86 ) {
83   - def className = pool.stringify(classNameEntry)
84   - def superclassName = pool.stringify(superclassNameEntry)
85   - def interfaces = interfaceEntries.map { pool.stringify(_) }
  87 + lazy val className = pool.stringify(classNameEntry)
  88 + lazy val superclassName = pool.stringify(superclassNameEntry)
  89 + lazy val interfaces = interfaceEntries.map { pool.stringify(_) }
86 90
87 91 def dump: String = {
88   - "class %s (%d.%d) %s\n".format(className, majorVersion, minorVersion, ClassAttributes.toString(accessFlags)) +
  92 + "class %s (%d.%d) %s\n".format(className, majorVersion, minorVersion, ClassFlags.toString(accessFlags)) +
89 93 "extends %s\n".format(superclassName) +
90 94 interfaces.map { iface => "with %s\n".format(iface) }.mkString +
91 95 "{\n" +
92   - fields.map { f => " field %s: %s %s\n".format(f.name, f.descriptor, FieldAttributes.toString(f.accessFlags)) }.mkString +
93   - methods.map { m => " method %s: %s %s\n".format(m.name, m.descriptor, MethodAttributes.toString(m.accessFlags)) }.mkString +
  96 + fields.map { f => " field %s: %s %s\n".format(f.name, f.descriptor, FieldFlags.toString(f.accessFlags)) }.mkString +
  97 + methods.map { m => " method %s: %s %s\n".format(m.name, m.descriptor, MethodFlags.toString(m.accessFlags)) }.mkString +
94 98 attributes.map { a => " attribute %s: %d bytes\n".format(a.name, a.size) }.mkString +
95 99 "}\n"
96 100 }
97 101 }
98 102
99   -object ClassAttributes {
  103 +/**
  104 + * Flags that may be set in the `accessFlags` field of a class.
  105 + */
  106 +object ClassFlags {
100 107 val ACC_PUBLIC = 0x0001
101 108 val ACC_FINAL = 0x0010
102 109 val ACC_SUPER = 0x0020
@@ -120,7 +127,10 @@ object ClassAttributes {
120 127 }
121 128 }
122 129
123   -object FieldAttributes {
  130 +/**
  131 + * Flags that may be set in the `accessFlags` field of a field.
  132 + */
  133 +object FieldFlags {
124 134 val ACC_PUBLIC = 0x0001
125 135 val ACC_PRIVATE = 0x0002
126 136 val ACC_PROTECTED = 0x0004
@@ -146,7 +156,10 @@ object FieldAttributes {
146 156 }
147 157 }
148 158
149   -object MethodAttributes {
  159 +/**
  160 + * Flags that may be set in the `accessFlags` field of a method.
  161 + */
  162 +object MethodFlags {
150 163 val ACC_PUBLIC = 0x0001
151 164 val ACC_PRIVATE = 0x0002
152 165 val ACC_PROTECTED = 0x0004
@@ -178,9 +191,16 @@ object MethodAttributes {
178 191 }
179 192 }
180 193
  194 +/**
  195 + * Tool for extracting basic data out of a java class file. This does not bother parsing bytecode
  196 + * or annotations, but instead focuses on the public API of a class.
  197 + *
  198 + * Strings are extracted from the data lazily, on demand, so the initial scan should require only
  199 + * as much time as it takes to parse the data segments.
  200 + */
181 201 object Deconstruct {
182 202 case class Attribute(pool: ConstantPool, nameEntry: ConstantPoolEntry, dataIndex: Int, size: Int) {
183   - def name = pool.stringify(nameEntry)
  203 + lazy val name = pool.stringify(nameEntry)
184 204 }
185 205
186 206 case class Field(
@@ -190,10 +210,13 @@ object Deconstruct {
190 210 accessFlags: Int,
191 211 attributes: Seq[Attribute]
192 212 ) {
193   - def name = pool.stringify(nameEntry)
194   - def descriptor = pool.stringify(descriptorEntry)
  213 + lazy val name = pool.stringify(nameEntry)
  214 + lazy val descriptor = pool.stringify(descriptorEntry)
195 215 }
196 216
  217 + /**
  218 + * Convert an input stream into a `ByteBuffer` and then extract the class description from it.
  219 + */
197 220 def apply(in: InputStream): ClassFile = {
198 221 val out = new ByteArrayOutputStream()
199 222 val buf = new Array[Byte](1 << 20)
@@ -205,14 +228,20 @@ object Deconstruct {
205 228 Deconstruct(out.toByteArray)
206 229 }
207 230
  231 + /**
  232 + * Convert a byte array into a `ByteBuffer` and then extract the class description from it.
  233 + */
208 234 def apply(data: Array[Byte]): ClassFile = apply(ByteBuffer.wrap(data))
209 235
  236 + /**
  237 + * Extract the class description from a `ByteBuffer` containing a java class file.
  238 + */
210 239 def apply(data: ByteBuffer): ClassFile = {
211 240 new Deconstruct(data).scan()
212 241 }
213 242 }
214 243
215   -class Deconstruct(data: ByteBuffer) {
  244 +private class Deconstruct(data: ByteBuffer) {
216 245 import Deconstruct._
217 246 import ConstantPoolEntry._
218 247
71 src/test/scala/com/twitter/deconstruct/ClassSignatureSpec.scala
... ... @@ -0,0 +1,71 @@
  1 +package com.twitter.deconstruct
  2 +
  3 +import org.scalatest._
  4 +
  5 +class ClassSignatureSpec extends FunSpec {
  6 + val classfile = Deconstruct(getClass.getClassLoader.getResourceAsStream("JournalFile.class"))
  7 + // give it at least one field.
  8 + val sig = ClassSignature(classfile).copy(
  9 + fields = Seq(
  10 + FieldSignature("fake", "J", FieldFlags.ACC_PUBLIC),
  11 + FieldSignature("fake2", "J", FieldFlags.ACC_PUBLIC)
  12 + )
  13 + )
  14 +
  15 + describe("ClassSignature") {
  16 + it("JournalFile.class") {
  17 + val expect = Seq(
  18 + "com/twitter/libkestrel/JournalFile(00000031):java/lang/Object:{}",
  19 + "F:fake(00000001):J",
  20 + "F:fake2(00000001):J",
  21 + "M:open(00000019):(Ljava/io/File;)Lcom/twitter/libkestrel/JournalFileReader;",
  22 + "M:create(00000019):(Ljava/io/File;Ljava/util/concurrent/ScheduledExecutorService;" +
  23 + "Lcom/twitter/util/Duration;Lcom/twitter/util/StorageUnit;)" +
  24 + "Lcom/twitter/libkestrel/JournalFileWriter;",
  25 + "M:append(00000019):(Ljava/io/File;Ljava/util/concurrent/ScheduledExecutorService;" +
  26 + "Lcom/twitter/util/Duration;Lcom/twitter/util/StorageUnit;)" +
  27 + "Lcom/twitter/libkestrel/JournalFileWriter;",
  28 + "M:HEADER_WRITER(00000019):()I"
  29 + )
  30 + assert(sig.signature === expect.mkString("", "\n", "\n"))
  31 + }
  32 +
  33 + describe("checks compatibility") {
  34 + it("when classes are identical") {
  35 + assert(sig.isCompatibleWith(ClassSignature(classfile)))
  36 + }
  37 +
  38 + it("when a field is added") {
  39 + val newsig = sig.copy(fields = sig.fields ++ Seq(FieldSignature("hork", "()I", 0)))
  40 + assert(newsig.isCompatibleWith(sig))
  41 + }
  42 +
  43 + it("when a method is added") {
  44 + val newsig = sig.copy(methods = sig.methods ++ Seq(FieldSignature("hork", "()I", 0)))
  45 + assert(newsig.isCompatibleWith(sig))
  46 + }
  47 +
  48 + it("not when a field is changed") {
  49 + val newhead = FieldSignature(sig.fields.head.name, sig.fields.head.descriptor, 0)
  50 + val newsig = sig.copy(fields = Seq(newhead) ++ sig.fields.tail)
  51 + assert(!newsig.isCompatibleWith(sig))
  52 + }
  53 +
  54 + it("not when a method is changed") {
  55 + val newhead = FieldSignature(sig.methods.head.name, sig.methods.head.descriptor, 0)
  56 + val newsig = sig.copy(methods = Seq(newhead) ++ sig.methods.tail)
  57 + assert(!newsig.isCompatibleWith(sig))
  58 + }
  59 +
  60 + it("not when a field is removed") {
  61 + val newsig = sig.copy(fields = sig.fields.tail)
  62 + assert(!newsig.isCompatibleWith(sig))
  63 + }
  64 +
  65 + it("not when a method is remoned") {
  66 + val newsig = sig.copy(methods = sig.methods.tail)
  67 + assert(!newsig.isCompatibleWith(sig))
  68 + }
  69 + }
  70 + }
  71 +}
13 src/test/scala/com/twitter/deconstruct/DeconstructSpec.scala
@@ -3,13 +3,12 @@ package com.twitter.deconstruct
3 3 import org.scalatest._
4 4
5 5 class DeconstructSpec extends FunSpec {
6   -
7 6 describe("Deconstruct") {
8 7 it("JournalFile.class") {
9 8 val classfile = Deconstruct(getClass.getClassLoader.getResourceAsStream("JournalFile.class"))
10 9
11   - assert((classfile.accessFlags & ClassAttributes.ACC_PUBLIC) != 0)
12   - assert((classfile.accessFlags & ClassAttributes.ACC_SYNTHETIC) === 0)
  10 + assert((classfile.accessFlags & ClassFlags.ACC_PUBLIC) != 0)
  11 + assert((classfile.accessFlags & ClassFlags.ACC_SYNTHETIC) === 0)
13 12 assert(classfile.className === "com/twitter/libkestrel/JournalFile")
14 13 assert(classfile.superclassName === "java/lang/Object")
15 14 assert(classfile.interfaces.size === 0)
@@ -21,8 +20,8 @@ class DeconstructSpec extends FunSpec {
21 20 assert(classfile.methods.size === 4)
22 21
23 22 assert(classfile.methods(0).name === "open")
24   - assert((classfile.methods(0).accessFlags & MethodAttributes.ACC_PUBLIC) != 0)
25   - assert((classfile.methods(0).accessFlags & MethodAttributes.ACC_SYNTHETIC) === 0)
  23 + assert((classfile.methods(0).accessFlags & MethodFlags.ACC_PUBLIC) != 0)
  24 + assert((classfile.methods(0).accessFlags & MethodFlags.ACC_SYNTHETIC) === 0)
26 25 assert(classfile.methods(0).descriptor === "(Ljava/io/File;)Lcom/twitter/libkestrel/JournalFileReader;")
27 26
28 27 }
@@ -51,12 +50,12 @@ class DeconstructSpec extends FunSpec {
51 50
52 51 assert(classfile.fields.size === 23)
53 52 assert(classfile.fields(1).name === "readerConfig")
54   - assert(classfile.fields(1).accessFlags === (FieldAttributes.ACC_PRIVATE | FieldAttributes.ACC_FINAL))
  53 + assert(classfile.fields(1).accessFlags === (FieldFlags.ACC_PRIVATE | FieldFlags.ACC_FINAL))
55 54 assert(classfile.fields(1).descriptor === "Lcom/twitter/libkestrel/config/JournaledQueueReaderConfig;")
56 55
57 56 assert(classfile.methods.size === 61)
58 57 assert(classfile.methods(25).name == "openItems")
59   - assert(classfile.methods(25).accessFlags == MethodAttributes.ACC_PUBLIC)
  58 + assert(classfile.methods(25).accessFlags == MethodFlags.ACC_PUBLIC)
60 59 assert(classfile.methods(25).descriptor == "()I")
61 60 }
62 61 }

No commit comments for this range

Something went wrong with that request. Please try again.