/** Parses a {@code LineNumberTable} attribute. */
  private Attribute lineNumberTable(
      DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length < 2) {
      return throwSeverelyTruncated();
    }

    ByteArray bytes = cf.getBytes();
    int count = bytes.getUnsignedShort(offset); // line_number_table_length

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "line_number_table_length: " + Hex.u2(count));
    }

    offset += 2;
    length -= 2;

    if (length != (count * 4)) {
      throwBadLength((count * 4) + 2);
    }

    LineNumberList list = new LineNumberList(count);

    for (int i = 0; i < count; i++) {
      int startPc = bytes.getUnsignedShort(offset);
      int lineNumber = bytes.getUnsignedShort(offset + 2);
      list.set(i, startPc, lineNumber);
      if (observer != null) {
        observer.parsed(bytes, offset, 4, Hex.u2(startPc) + " " + lineNumber);
      }
      offset += 4;
    }

    list.setImmutable();
    return new AttLineNumberTable(list);
  }
  /** Parses an {@code InnerClasses} attribute. */
  private Attribute innerClasses(
      DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length < 2) {
      return throwSeverelyTruncated();
    }

    ByteArray bytes = cf.getBytes();
    ConstantPool pool = cf.getConstantPool();
    int count = bytes.getUnsignedShort(offset); // number_of_classes

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "number_of_classes: " + Hex.u2(count));
    }

    offset += 2;
    length -= 2;

    if (length != (count * 8)) {
      throwBadLength((count * 8) + 2);
    }

    InnerClassList list = new InnerClassList(count);

    for (int i = 0; i < count; i++) {
      int innerClassIdx = bytes.getUnsignedShort(offset);
      int outerClassIdx = bytes.getUnsignedShort(offset + 2);
      int nameIdx = bytes.getUnsignedShort(offset + 4);
      int accessFlags = bytes.getUnsignedShort(offset + 6);
      CstType innerClass = (CstType) pool.get(innerClassIdx);
      CstType outerClass = (CstType) pool.get0Ok(outerClassIdx);
      CstString name = (CstString) pool.get0Ok(nameIdx);
      list.set(i, innerClass, outerClass, name, accessFlags);
      if (observer != null) {
        observer.parsed(
            bytes, offset, 2, "inner_class: " + DirectClassFile.stringOrNone(innerClass));
        observer.parsed(
            bytes, offset + 2, 2, "  outer_class: " + DirectClassFile.stringOrNone(outerClass));
        observer.parsed(bytes, offset + 4, 2, "  name: " + DirectClassFile.stringOrNone(name));
        observer.parsed(
            bytes, offset + 6, 2, "  access_flags: " + AccessFlags.innerClassString(accessFlags));
      }
      offset += 8;
    }

    list.setImmutable();
    return new AttInnerClasses(list);
  }
  /** Parses an {@code EnclosingMethod} attribute. */
  private Attribute enclosingMethod(
      DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length != 4) {
      throwBadLength(4);
    }

    ByteArray bytes = cf.getBytes();
    ConstantPool pool = cf.getConstantPool();

    int idx = bytes.getUnsignedShort(offset);
    CstType type = (CstType) pool.get(idx);

    idx = bytes.getUnsignedShort(offset + 2);
    CstNat method = (CstNat) pool.get0Ok(idx);

    Attribute result = new AttEnclosingMethod(type, method);

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "class: " + type);
      observer.parsed(bytes, offset + 2, 2, "method: " + DirectClassFile.stringOrNone(method));
    }

    return result;
  }
  /** Parses a {@code SourceFile} attribute. */
  private Attribute sourceFile(DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length != 2) {
      throwBadLength(2);
    }

    ByteArray bytes = cf.getBytes();
    ConstantPool pool = cf.getConstantPool();
    int idx = bytes.getUnsignedShort(offset);
    CstString cst = (CstString) pool.get(idx);
    Attribute result = new AttSourceFile(cst);

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "source: " + cst);
    }

    return result;
  }
  /** Parses a {@code LocalVariableTypeTable} attribute. */
  private Attribute localVariableTypeTable(
      DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length < 2) {
      return throwSeverelyTruncated();
    }

    ByteArray bytes = cf.getBytes();
    int count = bytes.getUnsignedShort(offset);

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "local_variable_type_table_length: " + Hex.u2(count));
    }

    LocalVariableList list =
        parseLocalVariables(
            bytes.slice(offset + 2, offset + length), cf.getConstantPool(), observer, count, true);
    return new AttLocalVariableTypeTable(list);
  }
  /** Parses a {@code ConstantValue} attribute. */
  private Attribute constantValue(
      DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length != 2) {
      return throwBadLength(2);
    }

    ByteArray bytes = cf.getBytes();
    ConstantPool pool = cf.getConstantPool();
    int idx = bytes.getUnsignedShort(offset);
    TypedConstant cst = (TypedConstant) pool.get(idx);
    Attribute result = new AttConstantValue(cst);

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "value: " + cst);
    }

    return result;
  }
  /** Parses an {@code Exceptions} attribute. */
  private Attribute exceptions(DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length < 2) {
      return throwSeverelyTruncated();
    }

    ByteArray bytes = cf.getBytes();
    int count = bytes.getUnsignedShort(offset); // number_of_exceptions

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "number_of_exceptions: " + Hex.u2(count));
    }

    offset += 2;
    length -= 2;

    if (length != (count * 2)) {
      throwBadLength((count * 2) + 2);
    }

    TypeList list = cf.makeTypeList(offset, count);
    return new AttExceptions(list);
  }
  /** Parses a {@code bootstrap} attribute. */
  private Attribute bootstrap(DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length < 2) {
      throwBadLength(2);
    }

    ByteArray bytes = cf.getBytes();
    ConstantPool pool = cf.getConstantPool();
    int count = bytes.getUnsignedShort(offset);

    BootstrapMethodList list =
        parseBootstrapMethods(
            bytes.slice(offset + 2, offset + length), cf.getConstantPool(), observer, count);

    if (observer != null) {
      //            observer.parsed(bytes, offset, 2, "source: " + cst);
    }

    AttBootstrapMethods attBootstrapMethods = new AttBootstrapMethods(list);

    // -6 for header (cf. 4.7.21)
    offset += attBootstrapMethods.byteLength() - 6;

    return attBootstrapMethods;
  }
  /** Parses a {@code Code} attribute. */
  private Attribute code(DirectClassFile cf, int offset, int length, ParseObserver observer) {
    if (length < 12) {
      return throwSeverelyTruncated();
    }

    ByteArray bytes = cf.getBytes();
    ConstantPool pool = cf.getConstantPool();
    int maxStack = bytes.getUnsignedShort(offset); // u2 max_stack
    int maxLocals = bytes.getUnsignedShort(offset + 2); // u2 max_locals
    int codeLength = bytes.getInt(offset + 4); // u4 code_length
    int origOffset = offset;

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "max_stack: " + Hex.u2(maxStack));
      observer.parsed(bytes, offset + 2, 2, "max_locals: " + Hex.u2(maxLocals));
      observer.parsed(bytes, offset + 4, 4, "code_length: " + Hex.u4(codeLength));
    }

    offset += 8;
    length -= 8;

    if (length < (codeLength + 4)) {
      return throwTruncated();
    }

    int codeOffset = offset;
    offset += codeLength;
    length -= codeLength;
    BytecodeArray code = new BytecodeArray(bytes.slice(codeOffset, codeOffset + codeLength), pool);
    if (observer != null) {
      code.forEach(new CodeObserver(code.getBytes(), observer));
    }

    // u2 exception_table_length
    int exceptionTableLength = bytes.getUnsignedShort(offset);
    ByteCatchList catches =
        (exceptionTableLength == 0) ? ByteCatchList.EMPTY : new ByteCatchList(exceptionTableLength);

    if (observer != null) {
      observer.parsed(bytes, offset, 2, "exception_table_length: " + Hex.u2(exceptionTableLength));
    }

    offset += 2;
    length -= 2;

    if (length < (exceptionTableLength * 8 + 2)) {
      return throwTruncated();
    }

    for (int i = 0; i < exceptionTableLength; i++) {
      if (observer != null) {
        observer.changeIndent(1);
      }

      int startPc = bytes.getUnsignedShort(offset);
      int endPc = bytes.getUnsignedShort(offset + 2);
      int handlerPc = bytes.getUnsignedShort(offset + 4);
      int catchTypeIdx = bytes.getUnsignedShort(offset + 6);
      CstType catchType = (CstType) pool.get0Ok(catchTypeIdx);
      catches.set(i, startPc, endPc, handlerPc, catchType);
      if (observer != null) {
        observer.parsed(
            bytes,
            offset,
            8,
            Hex.u2(startPc)
                + ".."
                + Hex.u2(endPc)
                + " -> "
                + Hex.u2(handlerPc)
                + " "
                + ((catchType == null) ? "<any>" : catchType.toHuman()));
      }
      offset += 8;
      length -= 8;

      if (observer != null) {
        observer.changeIndent(-1);
      }
    }

    catches.setImmutable();

    AttributeListParser parser = new AttributeListParser(cf, CTX_CODE, offset, this);
    parser.setObserver(observer);

    StdAttributeList attributes = parser.getList();
    attributes.setImmutable();

    int attributeByteCount = parser.getEndOffset() - offset;
    if (attributeByteCount != length) {
      return throwBadLength(attributeByteCount + (offset - origOffset));
    }

    return new AttCode(maxStack, maxLocals, code, catches, attributes);
  }