/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * Copyright 2014-2015 ForgeRock AS */ package org.opends.server.replication.protocol; import java.util.Collection; import java.util.zip.DataFormatException; import org.forgerock.opendj.io.ASN1; import org.forgerock.opendj.io.ASN1Reader; import org.forgerock.opendj.ldap.ByteSequenceReader; import org.forgerock.opendj.ldap.ByteString; import org.opends.server.replication.common.CSN; import org.opends.server.replication.common.ServerState; import org.opends.server.types.DN; import org.opends.server.types.DirectoryException; /** * Byte array scanner class helps decode data from byte arrays received via * messages over the replication protocol. Built on top of * {@link ByteSequenceReader}, it isolates the latter against legacy type * conversions from the replication protocol. * * @see ByteArrayBuilder ByteArrayBuilder class that encodes messages read with * current class. */ public class ByteArrayScanner { private final ByteSequenceReader bytes; private final byte[] byteArray; /** * Builds a ByteArrayScanner object that will read from the supplied byte * array. * * @param bytes * the byte array input that will be read from */ public ByteArrayScanner(byte[] bytes) { this.bytes = ByteString.wrap(bytes).asReader(); this.byteArray = bytes; } /** * Reads the next boolean. * * @return the next boolean * @throws DataFormatException * if no more data can be read from the input */ public boolean nextBoolean() throws DataFormatException { return nextByte() != 0; } /** * Reads the next byte. * * @return the next byte * @throws DataFormatException * if no more data can be read from the input */ public byte nextByte() throws DataFormatException { try { return bytes.readByte(); } catch (IndexOutOfBoundsException e) { throw new DataFormatException(e.getMessage()); } } /** * Reads the next short. * * @return the next short * @throws DataFormatException * if no more data can be read from the input */ public short nextShort() throws DataFormatException { try { return bytes.readShort(); } catch (IndexOutOfBoundsException e) { throw new DataFormatException(e.getMessage()); } } /** * Reads the next int. * * @return the next int * @throws DataFormatException * if no more data can be read from the input */ public int nextInt() throws DataFormatException { try { return bytes.readInt(); } catch (IndexOutOfBoundsException e) { throw new DataFormatException(e.getMessage()); } } /** * Reads the next long. * * @return the next long * @throws DataFormatException * if no more data can be read from the input */ public long nextLong() throws DataFormatException { try { return bytes.readLong(); } catch (IndexOutOfBoundsException e) { throw new DataFormatException(e.getMessage()); } } /** * Reads the next int that was encoded as a UTF8 string. * * @return the next int that was encoded as a UTF8 string. * @throws DataFormatException * if no more data can be read from the input */ public int nextIntUTF8() throws DataFormatException { return Integer.valueOf(nextString()); } /** * Reads the next long that was encoded as a UTF8 string. * * @return the next long that was encoded as a UTF8 string. * @throws DataFormatException * if no more data can be read from the input */ public long nextLongUTF8() throws DataFormatException { return Long.valueOf(nextString()); } /** * Reads the next UTF8-encoded string. * * @return the next UTF8-encoded string or null if the string length is zero * @throws DataFormatException * if no more data can be read from the input */ public String nextString() throws DataFormatException { try { final int offset = findZeroSeparator(); if (offset > 0) { final String s = bytes.readStringUtf8(offset); skipZeroSeparator(); return s; } skipZeroSeparator(); return null; } catch (IndexOutOfBoundsException e) { throw new DataFormatException(e.getMessage()); } } private int findZeroSeparator() throws DataFormatException { int offset = 0; final int remaining = bytes.remaining(); while (bytes.peek(offset) != 0 && offset < remaining) { offset++; } if (offset == remaining) { throw new DataFormatException("No more data to read from"); } return offset; } /** * Reads the next UTF8-encoded strings in the provided collection. * * @param output * the collection where to add the next UTF8-encoded strings * @param <TCol> * the collection's concrete type * @return the provided collection where the next UTF8-encoded strings have * been added. * @throws DataFormatException * if no more data can be read from the input */ public <TCol extends Collection<String>> TCol nextStrings(TCol output) throws DataFormatException { // nextInt() would have been safer, but byte is compatible with legacy code. final int colSize = nextByte(); for (int i = 0; i < colSize; i++) { output.add(nextString()); } return output; } /** * Reads the next CSN. * * @return the next CSN. * @throws DataFormatException * if CSN was incorrectly encoded or no more data can be read from * the input */ public CSN nextCSN() throws DataFormatException { try { return CSN.valueOf(bytes.readByteSequence(CSN.BYTE_ENCODING_LENGTH)); } catch (IndexOutOfBoundsException e) { throw new DataFormatException(e.getMessage()); } } /** * Reads the next CSN that was encoded as a UTF8 string. * * @return the next CSN that was encoded as a UTF8 string. * @throws DataFormatException * if legacy CSN was incorrectly encoded or no more data can be read * from the input */ public CSN nextCSNUTF8() throws DataFormatException { try { return CSN.valueOf(nextString()); } catch (IndexOutOfBoundsException e) { throw new DataFormatException(e.getMessage()); } } /** * Reads the next DN. * * @return the next DN. * @throws DataFormatException * if DN was incorrectly encoded or no more data can be read from * the input */ public DN nextDN() throws DataFormatException { try { return DN.valueOf(nextString()); } catch (DirectoryException e) { throw new DataFormatException(e.getLocalizedMessage()); } } /** * Return a new byte array containing all remaining bytes in this * ByteArrayScanner. * * @return new byte array containing all remaining bytes */ public byte[] remainingBytes() { final int length = byteArray.length - bytes.position(); return nextByteArray(length); } /** * Return a new byte array containing all remaining bytes in this * ByteArrayScanner bar the last one which is a zero terminated byte * (compatible with legacy code). * * @return new byte array containing all remaining bytes bar the last one */ public byte[] remainingBytesZeroTerminated() { /* do not copy stupid legacy zero separator */ final int length = byteArray.length - (bytes.position() + 1); final byte[] result = nextByteArray(length); bytes.skip(1); // ignore last (supposedly) zero byte return result; } /** * Return a new byte array containing the requested number of bytes. * * @param length * the number of bytes to be read and copied to the new byte array. * @return new byte array containing the requested number of bytes. */ public byte[] nextByteArray(final int length) { final byte[] result = new byte[length]; System.arraycopy(byteArray, bytes.position(), result, 0, length); bytes.skip(length); return result; } /** * Reads the next ServerState. * <p> * Caution: ServerState MUST be the last field (see * {@link ByteArrayBuilder#appendServerStateMustComeLast(ServerState)} javadoc). * <p> * Note: the super long method name it is intentional: * nobody will want to use it, which is good because nobody should. * * @return the next ServerState. * @throws DataFormatException * if ServerState was incorrectly encoded or no more data can be * read from the input * @see ByteArrayBuilder#appendServerStateMustComeLast(ServerState) */ public ServerState nextServerStateMustComeLast() throws DataFormatException { final ServerState result = new ServerState(); final int maxPos = byteArray.length - 1 /* stupid legacy zero separator */; while (bytes.position() < maxPos) { final int serverId = nextIntUTF8(); final CSN csn = nextCSNUTF8(); if (serverId != csn.getServerId()) { throw new DataFormatException("Expected serverId=" + serverId + " to be the same as serverId for CSN=" + csn); } result.update(csn); } skipZeroSeparator(); return result; } /** * Skips the next byte and verifies it is effectively the zero separator. * * @throws DataFormatException * if the next byte is not the zero separator. */ public void skipZeroSeparator() throws DataFormatException { if (bytes.peek() != (byte) 0) { throw new DataFormatException("Expected a zero separator at position " + bytes.position() + " but found byte " + bytes.peek()); } bytes.skip(1); } /** * Returns a new ASN1Reader that will read bytes from this ByteArrayScanner. * * @return a new ASN1Reader that will read bytes from this ByteArrayScanner. */ public ASN1Reader getASN1Reader() { return ASN1.getReader(bytes); } /** * Returns whether the scanner has more bytes to consume. * * @return true if the scanner has more bytes to consume, false otherwise. */ public boolean isEmpty() { return bytes.remaining() == 0; } /** {@inheritDoc} */ @Override public String toString() { return bytes.toString(); } }