/* * Copyright (C) 2008, 2010 Google Inc. * Copyright (C) 2008, 2009 Robin Rosenberg * Copyright (C) 2008, 2020 Shawn O. Pearce 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. *

* This class is not thread safe and may issue multiple reads to the underlying * stream for each method call made. *

* 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. *

* 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. *

* 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. *

* Intended for use only in tests. * * @return The delimiter marker. */ static String delimiter() { return DELIM; } /** * Get the end marker. *

* 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. *

* Calls {@link #readString()} on the {@link PacketLineIn} until * {@link #END} is encountered. * * @since 5.4 * */ public static class PacketLineInIterator implements Iterable { private PacketLineIn in; private String current; PacketLineInIterator(PacketLineIn in) throws IOException { this.in = in; current = in.readString(); } @Override public Iterator 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; } }; } } }