All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.aerospike.client.admin.AdminCommand Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2012-2023 Aerospike, Inc.
 *
 * Portions may be licensed to Aerospike, Inc. under one or more contributor
 * license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
 *
 * 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 com.aerospike.client.admin;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.mindrot.jbcrypt.BCrypt;

import com.aerospike.client.AerospikeException;
import com.aerospike.client.Log;
import com.aerospike.client.ResultCode;
import com.aerospike.client.cluster.Cluster;
import com.aerospike.client.cluster.Connection;
import com.aerospike.client.cluster.Node;
import com.aerospike.client.command.Buffer;
import com.aerospike.client.command.Command;
import com.aerospike.client.policy.AdminPolicy;
import com.aerospike.client.policy.AuthMode;
import com.aerospike.client.util.ThreadLocalData;

public class AdminCommand {
	// Commands
	private static final byte AUTHENTICATE = 0;
	private static final byte CREATE_USER = 1;
	private static final byte DROP_USER = 2;
	private static final byte SET_PASSWORD = 3;
	private static final byte CHANGE_PASSWORD = 4;
	private static final byte GRANT_ROLES = 5;
	private static final byte REVOKE_ROLES = 6;
	private static final byte QUERY_USERS = 9;
	private static final byte CREATE_ROLE = 10;
	private static final byte DROP_ROLE = 11;
	private static final byte GRANT_PRIVILEGES = 12;
	private static final byte REVOKE_PRIVILEGES = 13;
	private static final byte SET_WHITELIST = 14;
	private static final byte SET_QUOTAS = 15;
	private static final byte QUERY_ROLES = 16;
	private static final byte LOGIN = 20;

	// Field IDs
	private static final byte USER = 0;
	private static final byte PASSWORD = 1;
	private static final byte OLD_PASSWORD = 2;
	private static final byte CREDENTIAL = 3;
	private static final byte CLEAR_PASSWORD = 4;
	private static final byte SESSION_TOKEN = 5;
	private static final byte SESSION_TTL = 6;
	private static final byte ROLES = 10;
	private static final byte ROLE = 11;
	private static final byte PRIVILEGES = 12;
	private static final byte WHITELIST = 13;
	private static final byte READ_QUOTA = 14;
	private static final byte WRITE_QUOTA = 15;
	private static final byte READ_INFO = 16;
	private static final byte WRITE_INFO = 17;
	private static final byte CONNECTIONS = 18;

	// Misc
	private static final long MSG_VERSION = 2L;
	private static final long MSG_TYPE = 2L;
	private static final int FIELD_HEADER_SIZE = 5;
	private static final int HEADER_SIZE = 24;
	private static final int HEADER_REMAINING = 16;
	private static final int RESULT_CODE = 9;
	private static final int QUERY_END = 50;

	byte[] dataBuffer;
	int dataOffset;

	public AdminCommand() {
		this.dataBuffer = new byte[8192];
		dataOffset = 8;
	}

	public AdminCommand(byte[] dataBuffer) {
		this.dataBuffer = dataBuffer;
		dataOffset = 8;
	}

	public static class LoginCommand extends AdminCommand {
		public byte[] sessionToken = null;
		public long sessionExpiration = 0;

		public LoginCommand(Cluster cluster, Connection conn) throws IOException {
			super(ThreadLocalData.getBuffer());

			conn.setTimeout(cluster.loginTimeout);

			try {
				login(cluster, conn);
			}
			finally {
				conn.setTimeout(cluster.connectTimeout);
			}
		}

		private void login(Cluster cluster, Connection conn) throws IOException {
			if (cluster.authMode == AuthMode.INTERNAL) {
				writeHeader(LOGIN, 2);
				writeField(USER, cluster.getUser());
				writeField(CREDENTIAL, cluster.getPasswordHash());
			}
			else if (cluster.authMode == AuthMode.PKI) {
				writeHeader(LOGIN, 0);
			}
			else {
				writeHeader(LOGIN, 3);
				writeField(USER, cluster.getUser());
				writeField(CREDENTIAL, cluster.getPasswordHash());
				writeField(CLEAR_PASSWORD, cluster.getPassword());
			}
			writeSize();
			conn.write(dataBuffer, dataOffset);
			conn.readFully(dataBuffer, HEADER_SIZE);

			int result = dataBuffer[RESULT_CODE] & 0xFF;

			if (result != 0) {
				if (result == ResultCode.SECURITY_NOT_ENABLED) {
					// Server does not require login.
					return;
				}

				throw new AerospikeException(result, "Login failed");
			}

			// Read session token.
			long size = Buffer.bytesToLong(dataBuffer, 0);
			int receiveSize = ((int)(size & 0xFFFFFFFFFFFFL)) - HEADER_REMAINING;
			int fieldCount = dataBuffer[11] & 0xFF;

			if (receiveSize <= 0 || receiveSize > dataBuffer.length || fieldCount <= 0) {
				throw new AerospikeException(result, "Failed to retrieve session token");
			}

			conn.readFully(dataBuffer, receiveSize);
			dataOffset = 0;

			for (int i = 0; i < fieldCount; i++) {
				int len = Buffer.bytesToInt(dataBuffer, dataOffset);
				dataOffset += 4;
				int id = dataBuffer[dataOffset++] & 0xFF;
				len--;

				if (id == SESSION_TOKEN) {
					sessionToken = Arrays.copyOfRange(dataBuffer, dataOffset, dataOffset + len);
				}
				else if (id == SESSION_TTL) {
					// Subtract 60 seconds from ttl so client session expires before server session.
					long seconds = Buffer.bigUnsigned32ToLong(dataBuffer, dataOffset) - 60;

					if (seconds > 0) {
						sessionExpiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(seconds);
					}
					else {
						if (Log.warnEnabled()) {
							Log.warn("Invalid session TTL: " + seconds);
						}
					}
				}
				dataOffset += len;
			}

			if (sessionToken == null) {
				throw new AerospikeException(result, "Failed to retrieve session token");
			}
		}
	}

	public static boolean authenticate(Cluster cluster, Connection conn, byte[] sessionToken)
		throws IOException {
		AdminCommand command = new AdminCommand(ThreadLocalData.getBuffer());
		return command.authenticateSession(cluster, conn, sessionToken);
	}

	private boolean authenticateSession(Cluster cluster, Connection conn, byte[] sessionToken)
		throws IOException {

		dataOffset = 8;
		setAuthenticate(cluster, sessionToken);
		conn.write(dataBuffer, dataOffset);
		conn.readFully(dataBuffer, HEADER_SIZE, Command.STATE_READ_AUTH_HEADER);

		int result = dataBuffer[RESULT_CODE] & 0xFF;
		return result == 0 || result == ResultCode.SECURITY_NOT_ENABLED;
	}

	public int setAuthenticate(Cluster cluster, byte[] sessionToken) {
		if (cluster.authMode != AuthMode.PKI) {
			writeHeader(AUTHENTICATE, 2);
			writeField(USER, cluster.getUser());
		}
		else {
			writeHeader(AUTHENTICATE, 1);
		}
		writeField(SESSION_TOKEN, sessionToken);
		writeSize();
		return dataOffset;
	}

	public void createUser(Cluster cluster, AdminPolicy policy, String user, String password, List roles) {
		writeHeader(CREATE_USER, 3);
		writeField(USER, user);
		writeField(PASSWORD, password);
		writeRoles(roles);
		executeCommand(cluster, policy);
	}

	public void dropUser(Cluster cluster, AdminPolicy policy, String user) {
		writeHeader(DROP_USER, 1);
		writeField(USER, user);
		executeCommand(cluster, policy);
	}

	public void setPassword(Cluster cluster, AdminPolicy policy, byte[] user, String password) {
		writeHeader(SET_PASSWORD, 2);
		writeField(USER, user);
		writeField(PASSWORD, password);
		executeCommand(cluster, policy);
	}

	public void changePassword(Cluster cluster, AdminPolicy policy, byte[] user, String password) {
		writeHeader(CHANGE_PASSWORD, 3);
		writeField(USER, user);
		writeField(OLD_PASSWORD, cluster.getPasswordHash());
		writeField(PASSWORD, password);
		executeCommand(cluster, policy);
	}

	public void grantRoles(Cluster cluster, AdminPolicy policy, String user, List roles) {
		writeHeader(GRANT_ROLES, 2);
		writeField(USER, user);
		writeRoles(roles);
		executeCommand(cluster, policy);
	}

	public void revokeRoles(Cluster cluster, AdminPolicy policy, String user, List roles) {
		writeHeader(REVOKE_ROLES, 2);
		writeField(USER, user);
		writeRoles(roles);
		executeCommand(cluster, policy);
	}

	public void createRole(Cluster cluster, AdminPolicy policy, String roleName, List privileges) {
		writeHeader(CREATE_ROLE, 2);
		writeField(ROLE, roleName);
		writePrivileges(privileges);
		executeCommand(cluster, policy);
	}

	public void createRole(
		Cluster cluster,
		AdminPolicy policy,
		String roleName,
		List privileges,
		List whitelist,
		int readQuota,
		int writeQuota
	) {
		int fieldCount = 1;

		if (privileges != null && privileges.size() > 0) {
			fieldCount++;
		}

		if (whitelist != null && whitelist.size() > 0) {
			fieldCount++;
		}

		if (readQuota > 0) {
			fieldCount++;
		}

		if (writeQuota > 0) {
			fieldCount++;
		}

		writeHeader(CREATE_ROLE, fieldCount);
		writeField(ROLE, roleName);

		if (privileges != null && privileges.size() > 0) {
			writePrivileges(privileges);
		}

		if (whitelist != null && whitelist.size() > 0) {
			writeWhitelist(whitelist);
		}

		if (readQuota > 0) {
			writeField(READ_QUOTA, readQuota);
		}

		if (writeQuota > 0) {
			writeField(WRITE_QUOTA, writeQuota);
		}
		executeCommand(cluster, policy);
	}

	public void dropRole(Cluster cluster, AdminPolicy policy, String roleName) {
		writeHeader(DROP_ROLE, 1);
		writeField(ROLE, roleName);
		executeCommand(cluster, policy);
	}

	public void grantPrivileges(Cluster cluster, AdminPolicy policy, String roleName, List privileges) {
		writeHeader(GRANT_PRIVILEGES, 2);
		writeField(ROLE, roleName);
		writePrivileges(privileges);
		executeCommand(cluster, policy);
	}

	public void revokePrivileges(Cluster cluster, AdminPolicy policy, String roleName, List privileges) {
		writeHeader(REVOKE_PRIVILEGES, 2);
		writeField(ROLE, roleName);
		writePrivileges(privileges);
		executeCommand(cluster, policy);
	}

	public void setWhitelist(Cluster cluster, AdminPolicy policy, String roleName, List whitelist) {
		int fieldCount = (whitelist != null && whitelist.size() > 0) ? 2 : 1;

		writeHeader(SET_WHITELIST, fieldCount);
		writeField(ROLE, roleName);

		if (whitelist != null && whitelist.size() > 0) {
			writeWhitelist(whitelist);
		}

		executeCommand(cluster, policy);
	}

	public void setQuotas(Cluster cluster, AdminPolicy policy, String roleName, int readQuota, int writeQuota) {
		writeHeader(SET_QUOTAS, 3);
		writeField(ROLE, roleName);
		writeField(READ_QUOTA, readQuota);
		writeField(WRITE_QUOTA, writeQuota);
		executeCommand(cluster, policy);
	}

	private void writeRoles(List roles) {
		int offset = dataOffset + FIELD_HEADER_SIZE;
		dataBuffer[offset++] = (byte)roles.size();

		for (String role : roles) {
			int len = Buffer.stringToUtf8(role, dataBuffer, offset + 1);
			dataBuffer[offset] = (byte)len;
			offset += len + 1;
		}

		int size = offset - dataOffset - FIELD_HEADER_SIZE;
		writeFieldHeader(ROLES, size);
		dataOffset = offset;
	}

	private void writePrivileges(List privileges) {
		int offset = dataOffset + FIELD_HEADER_SIZE;
		dataBuffer[offset++] = (byte)privileges.size();

		for (Privilege privilege : privileges) {
			dataBuffer[offset++] = (byte)privilege.code.id;

			if (privilege.code.canScope()) {

				if (! (privilege.setName == null || privilege.setName.length() == 0) &&
					(privilege.namespace == null || privilege.namespace.length() == 0)) {
					throw new AerospikeException(ResultCode.INVALID_PRIVILEGE, "Admin privilege '" +
						privilege.code + "' has a set scope with an empty namespace.");
				}

				int len = Buffer.stringToUtf8(privilege.namespace, dataBuffer, offset + 1);
				dataBuffer[offset] = (byte)len;
				offset += len + 1;

				len = Buffer.stringToUtf8(privilege.setName, dataBuffer, offset + 1);
				dataBuffer[offset] = (byte)len;
				offset += len + 1;
			}
			else {
				if (! (privilege.namespace == null || privilege.namespace.length() == 0) ||
					! (privilege.setName == null || privilege.setName.length() == 0)) {
					throw new AerospikeException(ResultCode.INVALID_PRIVILEGE, "Admin global privilege '" +
						privilege.code + "' has namespace/set scope which is invalid.");
				}
			}
		}

		int size = offset - dataOffset - FIELD_HEADER_SIZE;
		writeFieldHeader(PRIVILEGES, size);
		dataOffset = offset;
	}

	private void writeWhitelist(List whitelist) {
		int offset = dataOffset + FIELD_HEADER_SIZE;
		boolean comma = false;

		for (String address : whitelist) {
			if (comma) {
				dataBuffer[offset++] = ',';
			}
			else {
				comma = true;
			}
			offset += Buffer.stringToUtf8(address, dataBuffer, offset);
		}

		int size = offset - dataOffset - FIELD_HEADER_SIZE;
		writeFieldHeader(WHITELIST, size);
		dataOffset = offset;
	}

	final void writeSize() {
		// Write total size of message which is the current offset.
		long size = ((long)dataOffset - 8) | (MSG_VERSION << 56) | (MSG_TYPE << 48);
		Buffer.longToBytes(size, dataBuffer, 0);
	}

	final void writeHeader(byte command, int fieldCount) {
		// Authenticate header is almost all zeros
		Arrays.fill(dataBuffer, dataOffset, dataOffset + 16, (byte)0);
		dataBuffer[dataOffset + 2] = command;
		dataBuffer[dataOffset + 3] = (byte)fieldCount;
		dataOffset += 16;
	}

	private void writeField(byte id, String str) {
		int len = Buffer.stringToUtf8(str, dataBuffer, dataOffset + FIELD_HEADER_SIZE);
		writeFieldHeader(id, len);
		dataOffset += len;
	}

	final void writeField(byte id, byte[] bytes) {
		System.arraycopy(bytes, 0, dataBuffer, dataOffset + FIELD_HEADER_SIZE, bytes.length);
		writeFieldHeader(id, bytes.length);
		dataOffset += bytes.length;
	}

	private void writeField(byte id, int val) {
		writeFieldHeader(id, 4);
		Buffer.intToBytes(val, dataBuffer, dataOffset);
		dataOffset += 4;
	}

	private void writeFieldHeader(byte id, int size) {
		Buffer.intToBytes(size + 1, dataBuffer, dataOffset);
		dataOffset += 4;
		dataBuffer[dataOffset++] = id;
	}

	private void executeCommand(Cluster cluster, AdminPolicy policy) {
		writeSize();
		Node node = cluster.getRandomNode();
		int timeout = (policy == null) ? 1000 : policy.timeout;
		Connection conn = node.getConnection(timeout);

		try {
			conn.write(dataBuffer, dataOffset);
			conn.readFully(dataBuffer, HEADER_SIZE);
			conn.updateLastUsed();
			node.putConnection(conn);
		}
		catch (Throwable e) {
			// All IO exceptions are considered fatal.  Do not retry.
			// Close socket to flush out possible garbage.  Do not put back in pool.
			node.closeConnection(conn);
			throw new AerospikeException(e);
		}

		int result = dataBuffer[RESULT_CODE] & 0xFF;

		if (result != 0)
		{
			throw new AerospikeException(result);
		}
	}

	private void executeQuery(Cluster cluster, AdminPolicy policy) {
		writeSize();
		Node node = cluster.getRandomNode();
		int timeout = (policy == null) ? 1000 : policy.timeout;
		int status = 0;
		Connection conn = node.getConnection(timeout);

		try {
			conn.write(dataBuffer, dataOffset);
			status = readBlocks(conn);
			node.putConnection(conn);
		}
		catch (Throwable e) {
			// Garbage may be in socket.  Do not put back into pool.
			node.closeConnection(conn);
			throw new AerospikeException(e);
		}

		if (status != QUERY_END && status > 0) {
			throw new AerospikeException(status, "Query failed.");
		}
	}

	private int readBlocks(Connection conn) throws IOException {
		int status = 0;

		while (status == 0)	{
			conn.readFully(dataBuffer, 8);
			long size = Buffer.bytesToLong(dataBuffer, 0);
			int receiveSize = ((int)(size & 0xFFFFFFFFFFFFL));

			if (receiveSize > 0) {
				if (receiveSize > dataBuffer.length) {
					dataBuffer = ThreadLocalData.resizeBuffer(receiveSize);
				}
				conn.readFully(dataBuffer, receiveSize);
				conn.updateLastUsed();
				status = parseBlock(receiveSize);
			}
		}
		return status;
	}

	public static String hashPassword(String password) {
		return BCrypt.hashpw(password, "$2a$10$7EqJtq98hPqEX7fNZaFWoO");
	}

	int parseBlock(int receiveSize) {
		return QUERY_END;
	}

	public static final class UserCommand extends AdminCommand {
		private final List list;

		public UserCommand(int capacity) {
			list = new ArrayList(capacity);
		}

		public User queryUser(Cluster cluster, AdminPolicy policy, String user) {
			super.writeHeader(QUERY_USERS, 1);
			super.writeField(USER, user);
			super.executeQuery(cluster, policy);
			return (list.size() > 0) ? list.get(0) : null;
		}

		public List queryUsers(Cluster cluster, AdminPolicy policy) {
			super.writeHeader(QUERY_USERS, 0);
			super.executeQuery(cluster, policy);
			return list;
		}

		@Override
		int parseBlock(int receiveSize)
		{
			super.dataOffset = 0;

			while (super.dataOffset < receiveSize) {
				int resultCode = super.dataBuffer[super.dataOffset + 1] & 0xFF;

				if (resultCode != 0) {
					return resultCode;
				}

				User user = new User();
				int fieldCount = super.dataBuffer[super.dataOffset + 3] & 0xFF;
				super.dataOffset += HEADER_REMAINING;

				for (int i = 0; i < fieldCount; i++) {
					int len = Buffer.bytesToInt(super.dataBuffer, super.dataOffset);
					super.dataOffset += 4;
					int id = super.dataBuffer[super.dataOffset++] & 0xFF;
					len--;

					switch (id) {
					case USER:
						user.name = Buffer.utf8ToString(super.dataBuffer, super.dataOffset, len);
						super.dataOffset += len;
						break;

					case ROLES:
						parseRoles(user);
						break;

					case READ_INFO:
						user.readInfo = parseInfo();
						break;

					case WRITE_INFO:
						user.writeInfo = parseInfo();
						break;

					case CONNECTIONS:
						user.connsInUse = Buffer.bytesToInt(super.dataBuffer, super.dataOffset);
						super.dataOffset += len;
						break;

					default:
						super.dataOffset += len;
						break;
					}
				}

				if (user.name == null && user.roles == null) {
					continue;
				}

				if (user.roles == null) {
					user.roles = new ArrayList(0);
				}
				list.add(user);
			}
			return 0;
		}

		private void parseRoles(User user) {
			int size = super.dataBuffer[super.dataOffset++] & 0xFF;
			user.roles = new ArrayList(size);

			for (int i = 0; i < size; i++) {
				int len = super.dataBuffer[super.dataOffset++] & 0xFF;
				String role = Buffer.utf8ToString(super.dataBuffer, super.dataOffset, len);
				super.dataOffset += len;
				user.roles.add(role);
			}
		}

		private List parseInfo() {
			int size = super.dataBuffer[super.dataOffset++] & 0xFF;
			ArrayList list = new ArrayList(size);

			for (int i = 0; i < size; i++) {
				int val = Buffer.bytesToInt(super.dataBuffer, super.dataOffset);
				super.dataOffset += 4;
				list.add(val);
			}
			return list;
		}
	}

	public static final class RoleCommand extends AdminCommand {
		private final List list;

		public RoleCommand(int capacity) {
			list = new ArrayList(capacity);
		}

		public Role queryRole(Cluster cluster, AdminPolicy policy, String roleName) {
			super.writeHeader(QUERY_ROLES, 1);
			super.writeField(ROLE, roleName);
			super.executeQuery(cluster, policy);
			return (list.size() > 0) ? list.get(0) : null;
		}

		public List queryRoles(Cluster cluster, AdminPolicy policy) {
			super.writeHeader(QUERY_ROLES, 0);
			super.executeQuery(cluster, policy);
			return list;
		}

		@Override
		int parseBlock(int receiveSize) {
			super.dataOffset = 0;

			while (super.dataOffset < receiveSize) {
				int resultCode = super.dataBuffer[super.dataOffset + 1] & 0xFF;

				if (resultCode != 0) {
					return resultCode;
				}

				Role role = new Role();
				int fieldCount = super.dataBuffer[super.dataOffset + 3] & 0xFF;
				super.dataOffset += HEADER_REMAINING;

				for (int i = 0; i < fieldCount; i++) {
					int len = Buffer.bytesToInt(super.dataBuffer, super.dataOffset);
					super.dataOffset += 4;
					int id = super.dataBuffer[super.dataOffset++] & 0xFF;
					len--;

					switch (id) {
					case ROLE:
						role.name = Buffer.utf8ToString(super.dataBuffer, super.dataOffset, len);
						super.dataOffset += len;
						break;

					case PRIVILEGES:
						parsePrivileges(role);
						break;

					case WHITELIST:
						role.whitelist = parseWhitelist(len);
						break;

					case READ_QUOTA:
						role.readQuota = Buffer.bytesToInt(super.dataBuffer, super.dataOffset);
						super.dataOffset += len;
						break;

					case WRITE_QUOTA:
						role.writeQuota = Buffer.bytesToInt(super.dataBuffer, super.dataOffset);
						super.dataOffset += len;
						break;

					default:
						super.dataOffset += len;
						break;
					}
				}

				if (role.name == null) {
					throw new AerospikeException(ResultCode.INVALID_ROLE);
				}

				if (role.privileges == null) {
					role.privileges = new ArrayList(0);
				}

				if (role.whitelist == null) {
					role.whitelist = new ArrayList(0);
				}

				list.add(role);
			}
			return 0;
		}

		private void parsePrivileges(Role role) {
			int size = super.dataBuffer[super.dataOffset++] & 0xFF;
			role.privileges = new ArrayList(size);

			for (int i = 0; i < size; i++) {
				Privilege priv = new Privilege();
				priv.code = PrivilegeCode.fromId(super.dataBuffer[super.dataOffset++] & 0xFF);

				if (priv.code.canScope()) {
					int len = super.dataBuffer[super.dataOffset++] & 0xFF;
					priv.namespace = Buffer.utf8ToString(super.dataBuffer, super.dataOffset, len);
					super.dataOffset += len;

					len = super.dataBuffer[super.dataOffset++] & 0xFF;
					priv.setName = Buffer.utf8ToString(super.dataBuffer, super.dataOffset, len);
					super.dataOffset += len;
				}
				role.privileges.add(priv);
			}
		}

		private List parseWhitelist(int len) {
			ArrayList list = new ArrayList();
			StringBuilder sb = new StringBuilder(256);
			int begin = super.dataOffset;
			int max = super.dataOffset + len;
			int l;

			while (super.dataOffset < max) {
				if (super.dataBuffer[super.dataOffset] == ',') {
					l = super.dataOffset - begin;

					if (l > 0) {
						String s = Buffer.utf8ToString(super.dataBuffer, begin, l, sb);
						list.add(s);
					}
					begin = ++super.dataOffset;
				}
				else {
					super.dataOffset++;
				}
			}
			l = super.dataOffset - begin;

			if (l > 0) {
				String s = Buffer.utf8ToString(super.dataBuffer, begin, l, sb);
				list.add(s);
			}
			return list;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy