| /* | |
| * Copyright (C) 2008, 2010 Google Inc. | |
| * Copyright (C) 2008, 2009 Robin Rosenberg <robin.rosenberg@dewire.com> | |
| * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others | |
| * | |
| * This program and the accompanying materials are made available under the | |
| * terms of the Eclipse Distribution License v. 1.0 which is available at | |
| * https://www.eclipse.org/org/documents/edl-v10.php. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| package org.eclipse.jgit.transport; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.UncheckedIOException; | |
| import java.text.MessageFormat; | |
| import java.util.Iterator; | |
| import org.eclipse.jgit.errors.PackProtocolException; | |
| import org.eclipse.jgit.internal.JGitText; | |
| import org.eclipse.jgit.lib.MutableObjectId; | |
| import org.eclipse.jgit.util.IO; | |
| import org.eclipse.jgit.util.RawParseUtils; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| /** | |
| * Read Git style pkt-line formatting from an input stream. | |
| * <p> | |
| * This class is not thread safe and may issue multiple reads to the underlying | |
| * stream for each method call made. | |
| * <p> | |
| * This class performs no buffering on its own. This makes it suitable to | |
| * interleave reads performed by this class with reads performed directly | |
| * against the underlying InputStream. | |
| */ | |
| public class PacketLineIn { | |
| private static final Logger log = LoggerFactory.getLogger(PacketLineIn.class); | |
| /** | |
| * Magic return from {@link #readString()} when a flush packet is found. | |
| */ | |
| private static final String END = new String(); /* must not string pool */ | |
| /** | |
| * Magic return from {@link #readString()} when a delim packet is found. | |
| */ | |
| private static final String DELIM = new String(); /* must not string pool */ | |
| enum AckNackResult { | |
| /** NAK */ | |
| NAK, | |
| /** ACK */ | |
| ACK, | |
| /** ACK + continue */ | |
| ACK_CONTINUE, | |
| /** ACK + common */ | |
| ACK_COMMON, | |
| /** ACK + ready */ | |
| ACK_READY; | |
| } | |
| private final byte[] lineBuffer = new byte[SideBandOutputStream.SMALL_BUF]; | |
| private final InputStream in; | |
| private long limit; | |
| /** | |
| * Create a new packet line reader. | |
| * | |
| * @param in | |
| * the input stream to consume. | |
| */ | |
| public PacketLineIn(InputStream in) { | |
| this(in, 0); | |
| } | |
| /** | |
| * Create a new packet line reader. | |
| * | |
| * @param in | |
| * the input stream to consume. | |
| * @param limit | |
| * bytes to read from the input; unlimited if set to 0. | |
| * @since 4.7 | |
| */ | |
| public PacketLineIn(InputStream in, long limit) { | |
| this.in = in; | |
| this.limit = limit; | |
| } | |
| /** | |
| * Parses a ACK/NAK line in protocol V2. | |
| * | |
| * @param line | |
| * to parse | |
| * @param returnedId | |
| * in case of {@link AckNackResult#ACK_COMMON ACK_COMMON} | |
| * @return one of {@link AckNackResult#NAK NAK}, | |
| * {@link AckNackResult#ACK_COMMON ACK_COMMON}, or | |
| * {@link AckNackResult#ACK_READY ACK_READY} | |
| * @throws IOException | |
| * on protocol or transport errors | |
| */ | |
| static AckNackResult parseACKv2(String line, MutableObjectId returnedId) | |
| throws IOException { | |
| if ("NAK".equals(line)) { //$NON-NLS-1$ | |
| return AckNackResult.NAK; | |
| } | |
| if (line.startsWith("ACK ") && line.length() == 44) { //$NON-NLS-1$ | |
| returnedId.fromString(line.substring(4, 44)); | |
| return AckNackResult.ACK_COMMON; | |
| } | |
| if ("ready".equals(line)) { //$NON-NLS-1$ | |
| return AckNackResult.ACK_READY; | |
| } | |
| if (line.startsWith("ERR ")) { //$NON-NLS-1$ | |
| throw new PackProtocolException(line.substring(4)); | |
| } | |
| throw new PackProtocolException( | |
| MessageFormat.format(JGitText.get().expectedACKNAKGot, line)); | |
| } | |
| AckNackResult readACK(MutableObjectId returnedId) throws IOException { | |
| final String line = readString(); | |
| if (line.length() == 0) | |
| throw new PackProtocolException(JGitText.get().expectedACKNAKFoundEOF); | |
| if ("NAK".equals(line)) //$NON-NLS-1$ | |
| return AckNackResult.NAK; | |
| if (line.startsWith("ACK ")) { //$NON-NLS-1$ | |
| returnedId.fromString(line.substring(4, 44)); | |
| if (line.length() == 44) | |
| return AckNackResult.ACK; | |
| final String arg = line.substring(44); | |
| switch (arg) { | |
| case " continue": //$NON-NLS-1$ | |
| return AckNackResult.ACK_CONTINUE; | |
| case " common": //$NON-NLS-1$ | |
| return AckNackResult.ACK_COMMON; | |
| case " ready": //$NON-NLS-1$ | |
| return AckNackResult.ACK_READY; | |
| default: | |
| break; | |
| } | |
| } | |
| if (line.startsWith("ERR ")) //$NON-NLS-1$ | |
| throw new PackProtocolException(line.substring(4)); | |
| throw new PackProtocolException(MessageFormat.format(JGitText.get().expectedACKNAKGot, line)); | |
| } | |
| /** | |
| * Read a single UTF-8 encoded string packet from the input stream. | |
| * <p> | |
| * If the string ends with an LF, it will be removed before returning the | |
| * value to the caller. If this automatic trimming behavior is not desired, | |
| * use {@link #readStringRaw()} instead. | |
| * | |
| * @return the string. {@link #END} if the string was the magic flush | |
| * packet, {@link #DELIM} if the string was the magic DELIM | |
| * packet. | |
| * @throws java.io.IOException | |
| * the stream cannot be read. | |
| */ | |
| public String readString() throws IOException { | |
| int len = readLength(); | |
| if (len == 0) { | |
| log.debug("git< 0000"); //$NON-NLS-1$ | |
| return END; | |
| } | |
| if (len == 1) { | |
| log.debug("git< 0001"); //$NON-NLS-1$ | |
| return DELIM; | |
| } | |
| len -= 4; // length header (4 bytes) | |
| if (len == 0) { | |
| log.debug("git< "); //$NON-NLS-1$ | |
| return ""; //$NON-NLS-1$ | |
| } | |
| byte[] raw; | |
| if (len <= lineBuffer.length) | |
| raw = lineBuffer; | |
| else | |
| raw = new byte[len]; | |
| IO.readFully(in, raw, 0, len); | |
| if (raw[len - 1] == '\n') | |
| len--; | |
| String s = RawParseUtils.decode(UTF_8, raw, 0, len); | |
| log.debug("git< " + s); //$NON-NLS-1$ | |
| return s; | |
| } | |
| /** | |
| * Get an iterator to read strings from the input stream. | |
| * | |
| * @return an iterator that calls {@link #readString()} until {@link #END} | |
| * is encountered. | |
| * | |
| * @throws IOException | |
| * on failure to read the initial packet line. | |
| * @since 5.4 | |
| */ | |
| public PacketLineInIterator readStrings() throws IOException { | |
| return new PacketLineInIterator(this); | |
| } | |
| /** | |
| * Read a single UTF-8 encoded string packet from the input stream. | |
| * <p> | |
| * Unlike {@link #readString()} a trailing LF will be retained. | |
| * | |
| * @return the string. {@link #END} if the string was the magic flush | |
| * packet. | |
| * @throws java.io.IOException | |
| * the stream cannot be read. | |
| */ | |
| public String readStringRaw() throws IOException { | |
| int len = readLength(); | |
| if (len == 0) { | |
| log.debug("git< 0000"); //$NON-NLS-1$ | |
| return END; | |
| } | |
| len -= 4; // length header (4 bytes) | |
| byte[] raw; | |
| if (len <= lineBuffer.length) | |
| raw = lineBuffer; | |
| else | |
| raw = new byte[len]; | |
| IO.readFully(in, raw, 0, len); | |
| String s = RawParseUtils.decode(UTF_8, raw, 0, len); | |
| log.debug("git< " + s); //$NON-NLS-1$ | |
| return s; | |
| } | |
| /** | |
| * Check if a string is the delimiter marker. | |
| * | |
| * @param s | |
| * the string to check | |
| * @return true if the given string is {@link #DELIM}, otherwise false. | |
| * @since 5.4 | |
| */ | |
| @SuppressWarnings({ "ReferenceEquality", "StringEquality" }) | |
| public static boolean isDelimiter(String s) { | |
| return s == DELIM; | |
| } | |
| /** | |
| * Get the delimiter marker. | |
| * <p> | |
| * Intended for use only in tests. | |
| * | |
| * @return The delimiter marker. | |
| */ | |
| static String delimiter() { | |
| return DELIM; | |
| } | |
| /** | |
| * Get the end marker. | |
| * <p> | |
| * Intended for use only in tests. | |
| * | |
| * @return The end marker. | |
| */ | |
| static String end() { | |
| return END; | |
| } | |
| /** | |
| * Check if a string is the packet end marker. | |
| * | |
| * @param s | |
| * the string to check | |
| * @return true if the given string is {@link #END}, otherwise false. | |
| * @since 5.4 | |
| */ | |
| @SuppressWarnings({ "ReferenceEquality", "StringEquality" }) | |
| public static boolean isEnd(String s) { | |
| return s == END; | |
| } | |
| void discardUntilEnd() throws IOException { | |
| for (;;) { | |
| int n = readLength(); | |
| if (n == 0) { | |
| break; | |
| } | |
| IO.skipFully(in, n - 4); | |
| } | |
| } | |
| int readLength() throws IOException { | |
| IO.readFully(in, lineBuffer, 0, 4); | |
| int len; | |
| try { | |
| len = RawParseUtils.parseHexInt16(lineBuffer, 0); | |
| } catch (ArrayIndexOutOfBoundsException err) { | |
| throw invalidHeader(err); | |
| } | |
| if (len == 0) { | |
| return 0; | |
| } else if (len == 1) { | |
| return 1; | |
| } else if (len < 4) { | |
| throw invalidHeader(); | |
| } | |
| if (limit != 0) { | |
| int n = len - 4; | |
| if (limit < n) { | |
| limit = -1; | |
| try { | |
| IO.skipFully(in, n); | |
| } catch (IOException e) { | |
| // Ignore failure discarding packet over limit. | |
| } | |
| throw new InputOverLimitIOException(); | |
| } | |
| // if set limit must not be 0 (means unlimited). | |
| limit = n < limit ? limit - n : -1; | |
| } | |
| return len; | |
| } | |
| private IOException invalidHeader() { | |
| return new IOException(MessageFormat.format(JGitText.get().invalidPacketLineHeader, | |
| "" + (char) lineBuffer[0] + (char) lineBuffer[1] //$NON-NLS-1$ | |
| + (char) lineBuffer[2] + (char) lineBuffer[3])); | |
| } | |
| private IOException invalidHeader(Throwable cause) { | |
| IOException ioe = invalidHeader(); | |
| ioe.initCause(cause); | |
| return ioe; | |
| } | |
| /** | |
| * IOException thrown by read when the configured input limit is exceeded. | |
| * | |
| * @since 4.7 | |
| */ | |
| public static class InputOverLimitIOException extends IOException { | |
| private static final long serialVersionUID = 1L; | |
| } | |
| /** | |
| * Iterator over packet lines. | |
| * <p> | |
| * Calls {@link #readString()} on the {@link PacketLineIn} until | |
| * {@link #END} is encountered. | |
| * | |
| * @since 5.4 | |
| * | |
| */ | |
| public static class PacketLineInIterator implements Iterable<String> { | |
| private PacketLineIn in; | |
| private String current; | |
| PacketLineInIterator(PacketLineIn in) throws IOException { | |
| this.in = in; | |
| current = in.readString(); | |
| } | |
| @Override | |
| public Iterator<String> iterator() { | |
| return new Iterator<>() { | |
| @Override | |
| public boolean hasNext() { | |
| return !PacketLineIn.isEnd(current); | |
| } | |
| @Override | |
| public String next() { | |
| String next = current; | |
| try { | |
| current = in.readString(); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException(e); | |
| } | |
| return next; | |
| } | |
| }; | |
| } | |
| } | |
| } |