/** * This file is part of git-as-svn. It is subject to the license terms * in the LICENSE file found in the top-level directory of this distribution * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn, * including this file, may be copied, modified, propagated, or distributed * except according to the terms contained in the LICENSE file. */ package svnserver.parser; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import svnserver.parser.token.*; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; /** * Интерфейс для чтения токенов из потока. * <p> * http://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol * * @author Artem V. Navrotskiy <bozaro@users.noreply.github.com> */ public class SvnServerParser { private static final int DEFAULT_BUFFER_SIZE = 32 * 1024; // Buffer size limit for out-of-memory prevention. private static final int MAX_BUFFER_SIZE = 10 * 1024 * 1024; @NotNull private final InputStream stream; private int depth = 0; @NotNull private final byte[] buffer; private int offset = 0; private int limit = 0; public SvnServerParser(@NotNull InputStream stream, int bufferSize) { this.stream = stream; this.buffer = new byte[Math.max(1, bufferSize)]; } public SvnServerParser(@NotNull InputStream stream) { this(stream, DEFAULT_BUFFER_SIZE); } @NotNull public String readText() throws IOException { return readToken(TextToken.class).getText(); } public int readNumber() throws IOException { return readToken(NumberToken.class).getNumber(); } public int getDepth() { return depth; } /** * Чтение элемента указанного типа из потока. * * @param tokenType Тип элемента. * @param <T> Тип элемента. * @return Прочитанный элемент. */ @NotNull public <T extends SvnServerToken> T readToken(@NotNull Class<T> tokenType) throws IOException { final SvnServerToken token = readToken(); if (!tokenType.isInstance(token)) { throw new IOException("Unexpected token: " + token + " (expected: " + tokenType.getName() + ')'); } //noinspection unchecked return (T) token; } /** * Чтение элемента списка из потока. * * @param tokenType Тип элемента. * @param <T> Тип элемента. * @return Прочитанный элемент. */ @Nullable public <T extends SvnServerToken> T readItem(@NotNull Class<T> tokenType) throws IOException { final SvnServerToken token = readToken(); if (ListEndToken.instance.equals(token)) { return null; } if (!tokenType.isInstance(token)) { throw new IOException("Unexpected token: " + token + " (expected: " + tokenType.getName() + ')'); } //noinspection unchecked return (T) token; } /** * Чтение элемента из потока. * * @return Возвращает элемент из потока. Если элемента нет - возвращает null. */ @SuppressWarnings("OverlyComplexMethod") @NotNull public SvnServerToken readToken() throws IOException { byte read = skipSpaces(); if (read == '(') { depth++; return ListBeginToken.instance; } if (read == ')') { depth--; if (depth < 0) { throw new IOException("Unexpect end of list token."); } return ListEndToken.instance; } // Чтение чисел и строк. if (isDigit(read)) { return readNumberToken(read); } // Обычная строчка. if (isAlpha(read)) { return readWord(); } throw new IOException("Unexpected character in stream: " + read + " (need 'a'..'z', 'A'..'Z', '0'..'9', ' ' or '\n')"); } private SvnServerToken readNumberToken(byte first) throws IOException { int result = first - '0'; while (true) { while (offset < limit) { final byte data = buffer[offset]; offset++; if ((data < '0') || (data > '9')) { if (data == ':') { return readString(result); } if (isSpace(data)) { return new NumberToken(result); } throw new IOException("Unexpected character in stream: " + data + " (need ' ', '\\n' or ':')"); } result = result * 10 + (data - '0'); } if (limit < 0) { throw new EOFException(); } offset = 0; limit = stream.read(buffer); } } private byte skipSpaces() throws IOException { while (true) { while (offset < limit) { final byte data = buffer[offset]; offset++; if (!isSpace(data)) { return data; } } if (limit < 0) { throw new EOFException(); } offset = 0; limit = stream.read(buffer); } } private static boolean isSpace(int data) { return (data == ' ') || (data == '\n'); } private static boolean isDigit(int data) { return (data >= '0' && data <= '9'); } @NotNull private StringToken readString(int length) throws IOException { if (length >= MAX_BUFFER_SIZE) { throw new IOException("Data is too long. Buffer overflow: " + buffer.length); } if (limit < 0) { throw new EOFException(); } final byte[] token = new byte[length]; if (length <= limit - offset) { System.arraycopy(buffer, offset, token, 0, length); offset += length; } else { int position = limit - offset; System.arraycopy(buffer, offset, token, 0, position); limit = 0; offset = 0; while (position < length) { int size = stream.read(token, position, length - position); if (size < 0) { limit = -1; throw new EOFException(); } position += size; } } return new StringToken(Arrays.copyOf(token, length)); } private static boolean isAlpha(int data) { return (data >= 'a' && data <= 'z') || (data >= 'A' && data <= 'Z'); } @NotNull private WordToken readWord() throws IOException { int begin = offset - 1; while (offset < limit) { final byte data = buffer[offset]; offset++; if (isSpace(data)) { return new WordToken(new String(buffer, begin, offset - begin - 1, StandardCharsets.US_ASCII)); } if (!(isAlpha(data) || isDigit(data) || (data == '-'))) { throw new IOException("Unexpected character in stream: " + data + " (need 'a'..'z', 'A'..'Z', '0'..'9' or '-')"); } } System.arraycopy(buffer, begin, buffer, 0, limit - begin); limit = offset - begin; offset = limit; while (limit < buffer.length) { int size = stream.read(buffer, limit, buffer.length - limit); if (size < 0) { throw new EOFException(); } limit += size; while (offset < limit) { final byte data = buffer[offset]; offset++; if (isSpace(data)) { return new WordToken(new String(buffer, 0, offset - 1, StandardCharsets.US_ASCII)); } if (!(isAlpha(data) || isDigit(data) || (data == '-'))) { throw new IOException("Unexpected character in stream: " + data + " (need 'a'..'z', 'A'..'Z', '0'..'9' or '-')"); } } } throw new IOException("Data is too long. Buffer overflow: " + buffer.length); } public void skipItems() throws IOException { int depth = 0; while (depth >= 0) { final SvnServerToken token = readToken(SvnServerToken.class); if (ListBeginToken.instance.equals(token)) { depth++; } if (ListEndToken.instance.equals(token)) { depth--; } } } }