docs: private boolean handshake()
      {
          assert (handshaking);
          assert (greetingRecv.position() < greetingSize);
  
          final Mechanisms mechanism = options.mechanism;
          assert (mechanism != null);
  
          //  Position of the version field in the greeting.
          final int revisionPos = SIGNATURE_SIZE;
  
          //  Make sure batch sizes match large buffer sizes
          final int inBatchSize = Math.max(options.rcvbuf, Config.IN_BATCH_SIZE.getValue());
          final int outBatchSize = Math.max(options.sndbuf, Config.OUT_BATCH_SIZE.getValue());
  
          //  Receive the greeting.
          while (greetingRecv.position() < greetingSize) {
              final int n = read(greetingRecv);
              if (n == 0) {
                  error(ErrorReason.CONNECTION);
                  return false;
              }
              if (n == -1) {
                  if (!errno.is(ZError.EAGAIN)) {
                      error(ErrorReason.CONNECTION);
                  }
                  return false;
              }
  
              //  We have received at least one byte from the peer.
              //  If the first byte is not 0xff, we know that the
              //  peer is using unversioned protocol.
              if ((greetingRecv.get(0) & 0xff) != 0xff) {
                  // If this first byte is not %FF,
                  // then the other peer is using ZMTP 1.0.
                  break;
              }
  
              if (greetingRecv.position() < SIGNATURE_SIZE) {
                  continue;
              }
  
              //  Inspect the right-most bit of the 10th byte (which coincides
              //  with the 'flags' field if a regular message was sent).
              //  Zero indicates this is a header of identity message
              //  (i.e. the peer is using the unversioned protocol).
              if ((greetingRecv.get(9) & 0x01) != 0x01) {
                  break;
              }
  
              // If the least significant bit is 1, the peer is using ZMTP 2.0 or later
              // and has sent us the ZMTP signature.
  
              int outpos = greetingSend.position();
              //  The peer is using versioned protocol.
              //  Send the major version number.
              if (greetingSend.limit() == SIGNATURE_SIZE) {
                  if (outsize == 0) {
                      ioObject.setPollOut(handle);
                  }
                  greetingSend.limit(SIGNATURE_SIZE + 1);
                  greetingSend.put(revisionPos, Protocol.V3.revision); //  Major version number
                  outsize += 1;
              }
  
              if (greetingRecv.position() > SIGNATURE_SIZE) {
                  if (greetingSend.limit() == SIGNATURE_SIZE + 1) {
                      if (outsize == 0) {
                          ioObject.setPollOut(handle);
                      }
                      // We read a further byte, which indicates the ZMTP version.
                      byte protocol = greetingRecv.get(revisionPos);
                      if (protocol == Protocol.V1.revision || protocol == Protocol.V2.revision) {
                          // If this is V1 or V2, we have a ZMTP 2.0 peer.
                          greetingSend.limit(V2_GREETING_SIZE);
                          greetingSend.position(SIGNATURE_SIZE + 1);
                          greetingSend.put((byte) options.type); // Socket type
                          outsize += 1;
                      }
                      else {
                          // If this is 3 or greater, we have a ZMTP 3.0 peer.
                          greetingSend.limit(V3_GREETING_SIZE);
                          greetingSend.position(SIGNATURE_SIZE + 1);
                          greetingSend.put((byte) 0); //  Minor version number
                          outsize += 1;
                          greetingSend.mark();
                          greetingSend.put(new byte[20]);
  
                          assert (mechanism == Mechanisms.NULL || mechanism == Mechanisms.PLAIN
                                  || mechanism == Mechanisms.CURVE || mechanism == Mechanisms.GSSAPI);
                          greetingSend.reset();
                          greetingSend.put(mechanism.name().getBytes(ZMQ.CHARSET));
                          greetingSend.reset();
                          greetingSend.position(greetingSend.position() + 20);
                          outsize += 20;
                          greetingSend.put(new byte[32]);
                          outsize += 32;
  
                          greetingSize = V3_GREETING_SIZE;
                      }
                  }
              }
              greetingSend.position(outpos);
          }
  
          //  Is the peer using the unversioned protocol?
          //  If so, we send and receive rest of identity
          //  messages.
          if ((greetingRecv.get(0) & 0xff) != 0xff || (greetingRecv.get(9) & 0x01) == 0) {
              // If this first byte is %FF, then we read nine further bytes,
              // and inspect the last byte (the 10th in total).
              // If the least significant bit is 0, then the other peer is using ZMTP 1.0.
              if (session.zapEnabled()) {
                  // reject ZMTP 1.0 connections if ZAP is enabled
                  error(ErrorReason.PROTOCOL);
                  return false;
              }
  
              zmtpVersion = Protocol.V0;
  
              encoder = new V1Encoder(errno, outBatchSize);
              decoder = new V1Decoder(errno, inBatchSize, options.maxMsgSize, options.allocator);
  
              //  We have already sent the message header.
              //  Since there is no way to tell the encoder to
              //  skip the message header, we simply throw that
              //  header data away.
              final int headerSize = options.identitySize + 1 >= 255 ? 10 : 2;
              ByteBuffer tmp = ByteBuffer.allocate(headerSize);
  
              //  Prepare the identity message and load it into encoder.
              //  Then consume bytes we have already sent to the peer.
              Msg txMsg = new Msg(options.identitySize);
              txMsg.put(options.identity, 0, options.identitySize);
              encoder.loadMsg(txMsg);
  
              ValueReference<ByteBuffer> bufferp = new ValueReference<>(tmp);
              int bufferSize = encoder.encode(bufferp, headerSize);
              assert (bufferSize == headerSize);
  
              //  Make sure the decoder sees the data we have already received.
              decodeDataAfterHandshake(0);
  
              //  To allow for interoperability with peers that do not forward
              //  their subscriptions, we inject a phantom subscription message
              //  message into the incoming message stream.
              if (options.type == ZMQ.ZMQ_PUB || options.type == ZMQ.ZMQ_XPUB) {
                  subscriptionRequired = true;
              }
  
              //  We are sending our identity now and the next message
              //  will come from the socket.
              nextMsg = pullMsgFromSession;
              //  We are expecting identity message.
              processMsg = processIdentity;
  
          }
          else if (greetingRecv.get(revisionPos) == Protocol.V1.revision) {
              //  ZMTP/1.0 framing.
  
              zmtpVersion = Protocol.V1;
  
              if (session.zapEnabled()) {
                  // reject ZMTP 1.0 connections if ZAP is enabled
                  error(ErrorReason.PROTOCOL);
                  return false;
              }
              encoder = new V1Encoder(errno, outBatchSize);
              decoder = new V1Decoder(errno, inBatchSize, options.maxMsgSize, options.allocator);
  
              decodeDataAfterHandshake(V2_GREETING_SIZE);
          }
          else if (greetingRecv.get(revisionPos) == Protocol.V2.revision) {
              //  ZMTP/2.0 framing.
  
              zmtpVersion = Protocol.V2;
  
              if (session.zapEnabled()) {
                  // reject ZMTP 2.0 connections if ZAP is enabled
                  error(ErrorReason.PROTOCOL);
                  return false;
              }
              encoder = new V2Encoder(errno, outBatchSize);
              decoder = new V2Decoder(errno, inBatchSize, options.maxMsgSize, options.allocator);
  
              decodeDataAfterHandshake(V2_GREETING_SIZE);
          }
          else {
              zmtpVersion = Protocol.V3;
  
              encoder = new V2Encoder(errno, outBatchSize);
              decoder = new V2Decoder(errno, inBatchSize, options.maxMsgSize, options.allocator);
  
              greetingRecv.position(V2_GREETING_SIZE);
              if (mechanism.isMechanism(greetingRecv)) {
                  this.mechanism = mechanism.create(session, peerAddress, options);
              }
              else {
                  error(ErrorReason.PROTOCOL);
                  return false;
              }
  
              nextMsg = nextHandshakeCommand;
              processMsg = processHandshakeCommand;
          }
  
          // Start polling for output if necessary.
          if (outsize == 0) {
              ioObject.setPollOut(handle);
          }
  
          //  Handshaking was successful.
          //  Switch into the normal message flow.
          handshaking = false;
  
          if (hasHandshakeTimer) {
              ioObject.cancelTimer(HANDSHAKE_TIMER_ID);
              hasHandshakeTimer = false;
          }
  
          socket.eventHandshaken(endpoint, zmtpVersion.ordinal());
  
          return true;
      }