View Javadoc
1   /*
2    * Copyright (C) 2008, 2010 Google Inc.
3    * Copyright (C) 2008, 2009 Robin Rosenberg <robin.rosenberg@dewire.com>
4    * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
5    *
6    * This program and the accompanying materials are made available under the
7    * terms of the Eclipse Distribution License v. 1.0 which is available at
8    * https://www.eclipse.org/org/documents/edl-v10.php.
9    *
10   * SPDX-License-Identifier: BSD-3-Clause
11   */
12  
13  package org.eclipse.jgit.transport;
14  
15  import static java.nio.charset.StandardCharsets.UTF_8;
16  
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.UncheckedIOException;
20  import java.text.MessageFormat;
21  import java.util.Iterator;
22  
23  import org.eclipse.jgit.errors.PackProtocolException;
24  import org.eclipse.jgit.internal.JGitText;
25  import org.eclipse.jgit.lib.MutableObjectId;
26  import org.eclipse.jgit.util.IO;
27  import org.eclipse.jgit.util.RawParseUtils;
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  
31  /**
32   * Read Git style pkt-line formatting from an input stream.
33   * <p>
34   * This class is not thread safe and may issue multiple reads to the underlying
35   * stream for each method call made.
36   * <p>
37   * This class performs no buffering on its own. This makes it suitable to
38   * interleave reads performed by this class with reads performed directly
39   * against the underlying InputStream.
40   */
41  public class PacketLineIn {
42  	private static final Logger log = LoggerFactory.getLogger(PacketLineIn.class);
43  
44  	/**
45  	 * Magic return from {@link #readString()} when a flush packet is found.
46  	 *
47  	 * @deprecated Callers should use {@link #isEnd(String)} to check if a
48  	 *             string is the end marker, or
49  	 *             {@link PacketLineIn#readStrings()} to iterate over all
50  	 *             strings in the input stream until the marker is reached.
51  	 */
52  	@Deprecated
53  	public static final String END = new String(); /* must not string pool */
54  
55  	/**
56  	 * Magic return from {@link #readString()} when a delim packet is found.
57  	 *
58  	 * @since 5.0
59  	 * @deprecated Callers should use {@link #isDelimiter(String)} to check if a
60  	 *             string is the delimiter.
61  	 */
62  	@Deprecated
63  	public static final String DELIM = new String(); /* must not string pool */
64  
65  	enum AckNackResult {
66  		/** NAK */
67  		NAK,
68  		/** ACK */
69  		ACK,
70  		/** ACK + continue */
71  		ACK_CONTINUE,
72  		/** ACK + common */
73  		ACK_COMMON,
74  		/** ACK + ready */
75  		ACK_READY;
76  	}
77  
78  	private final byte[] lineBuffer = new byte[SideBandOutputStream.SMALL_BUF];
79  	private final InputStream in;
80  	private long limit;
81  
82  	/**
83  	 * Create a new packet line reader.
84  	 *
85  	 * @param in
86  	 *            the input stream to consume.
87  	 */
88  	public PacketLineIn(InputStream in) {
89  		this(in, 0);
90  	}
91  
92  	/**
93  	 * Create a new packet line reader.
94  	 *
95  	 * @param in
96  	 *            the input stream to consume.
97  	 * @param limit
98  	 *            bytes to read from the input; unlimited if set to 0.
99  	 * @since 4.7
100 	 */
101 	public PacketLineIn(InputStream in, long limit) {
102 		this.in = in;
103 		this.limit = limit;
104 	}
105 
106 	/**
107 	 * Parses a ACK/NAK line in protocol V2.
108 	 *
109 	 * @param line
110 	 *            to parse
111 	 * @param returnedId
112 	 *            in case of {@link AckNackResult#ACK_COMMON ACK_COMMON}
113 	 * @return one of {@link AckNackResult#NAK NAK},
114 	 *         {@link AckNackResult#ACK_COMMON ACK_COMMON}, or
115 	 *         {@link AckNackResult#ACK_READY ACK_READY}
116 	 * @throws IOException
117 	 *             on protocol or transport errors
118 	 */
119 	static AckNackResult parseACKv2(String line, MutableObjectId returnedId)
120 			throws IOException {
121 		if ("NAK".equals(line)) { //$NON-NLS-1$
122 			return AckNackResult.NAK;
123 		}
124 		if (line.startsWith("ACK ") && line.length() == 44) { //$NON-NLS-1$
125 			returnedId.fromString(line.substring(4, 44));
126 			return AckNackResult.ACK_COMMON;
127 		}
128 		if ("ready".equals(line)) { //$NON-NLS-1$
129 			return AckNackResult.ACK_READY;
130 		}
131 		if (line.startsWith("ERR ")) { //$NON-NLS-1$
132 			throw new PackProtocolException(line.substring(4));
133 		}
134 		throw new PackProtocolException(
135 				MessageFormat.format(JGitText.get().expectedACKNAKGot, line));
136 	}
137 
138 	AckNackResult readACK(MutableObjectId returnedId) throws IOException {
139 		final String line = readString();
140 		if (line.length() == 0)
141 			throw new PackProtocolException(JGitText.get().expectedACKNAKFoundEOF);
142 		if ("NAK".equals(line)) //$NON-NLS-1$
143 			return AckNackResult.NAK;
144 		if (line.startsWith("ACK ")) { //$NON-NLS-1$
145 			returnedId.fromString(line.substring(4, 44));
146 			if (line.length() == 44)
147 				return AckNackResult.ACK;
148 
149 			final String arg = line.substring(44);
150 			switch (arg) {
151 			case " continue": //$NON-NLS-1$
152 				return AckNackResult.ACK_CONTINUE;
153 			case " common": //$NON-NLS-1$
154 				return AckNackResult.ACK_COMMON;
155 			case " ready": //$NON-NLS-1$
156 				return AckNackResult.ACK_READY;
157 			default:
158 				break;
159 			}
160 		}
161 		if (line.startsWith("ERR ")) //$NON-NLS-1$
162 			throw new PackProtocolException(line.substring(4));
163 		throw new PackProtocolException(MessageFormat.format(JGitText.get().expectedACKNAKGot, line));
164 	}
165 
166 	/**
167 	 * Read a single UTF-8 encoded string packet from the input stream.
168 	 * <p>
169 	 * If the string ends with an LF, it will be removed before returning the
170 	 * value to the caller. If this automatic trimming behavior is not desired,
171 	 * use {@link #readStringRaw()} instead.
172 	 *
173 	 * @return the string. {@link #END} if the string was the magic flush
174 	 *         packet, {@link #DELIM} if the string was the magic DELIM
175 	 *         packet.
176 	 * @throws java.io.IOException
177 	 *             the stream cannot be read.
178 	 */
179 	public String readString() throws IOException {
180 		int len = readLength();
181 		if (len == 0) {
182 			log.debug("git< 0000"); //$NON-NLS-1$
183 			return END;
184 		}
185 		if (len == 1) {
186 			log.debug("git< 0001"); //$NON-NLS-1$
187 			return DELIM;
188 		}
189 
190 		len -= 4; // length header (4 bytes)
191 		if (len == 0) {
192 			log.debug("git< "); //$NON-NLS-1$
193 			return ""; //$NON-NLS-1$
194 		}
195 
196 		byte[] raw;
197 		if (len <= lineBuffer.length)
198 			raw = lineBuffer;
199 		else
200 			raw = new byte[len];
201 
202 		IO.readFully(in, raw, 0, len);
203 		if (raw[len - 1] == '\n')
204 			len--;
205 
206 		String s = RawParseUtils.decode(UTF_8, raw, 0, len);
207 		log.debug("git< " + s); //$NON-NLS-1$
208 		return s;
209 	}
210 
211 	/**
212 	 * Get an iterator to read strings from the input stream.
213 	 *
214 	 * @return an iterator that calls {@link #readString()} until {@link #END}
215 	 *         is encountered.
216 	 *
217 	 * @throws IOException
218 	 *             on failure to read the initial packet line.
219 	 * @since 5.4
220 	 */
221 	public PacketLineInIterator readStrings() throws IOException {
222 		return new PacketLineInIterator(this);
223 	}
224 
225 	/**
226 	 * Read a single UTF-8 encoded string packet from the input stream.
227 	 * <p>
228 	 * Unlike {@link #readString()} a trailing LF will be retained.
229 	 *
230 	 * @return the string. {@link #END} if the string was the magic flush
231 	 *         packet.
232 	 * @throws java.io.IOException
233 	 *             the stream cannot be read.
234 	 */
235 	public String readStringRaw() throws IOException {
236 		int len = readLength();
237 		if (len == 0) {
238 			log.debug("git< 0000"); //$NON-NLS-1$
239 			return END;
240 		}
241 
242 		len -= 4; // length header (4 bytes)
243 
244 		byte[] raw;
245 		if (len <= lineBuffer.length)
246 			raw = lineBuffer;
247 		else
248 			raw = new byte[len];
249 
250 		IO.readFully(in, raw, 0, len);
251 
252 		String s = RawParseUtils.decode(UTF_8, raw, 0, len);
253 		log.debug("git< " + s); //$NON-NLS-1$
254 		return s;
255 	}
256 
257 	/**
258 	 * Check if a string is the delimiter marker.
259 	 *
260 	 * @param s
261 	 *            the string to check
262 	 * @return true if the given string is {@link #DELIM}, otherwise false.
263 	 * @since 5.4
264 	 */
265 	@SuppressWarnings({ "ReferenceEquality", "StringEquality" })
266 	public static boolean isDelimiter(String s) {
267 		return s == DELIM;
268 	}
269 
270 	/**
271 	 * Get the delimiter marker.
272 	 * <p>
273 	 * Intended for use only in tests.
274 	 *
275 	 * @return The delimiter marker.
276 	 */
277 	static String delimiter() {
278 		return DELIM;
279 	}
280 
281 	/**
282 	 * Get the end marker.
283 	 * <p>
284 	 * Intended for use only in tests.
285 	 *
286 	 * @return The end marker.
287 	 */
288 	static String end() {
289 		return END;
290 	}
291 
292 	/**
293 	 * Check if a string is the packet end marker.
294 	 *
295 	 * @param s
296 	 *            the string to check
297 	 * @return true if the given string is {@link #END}, otherwise false.
298 	 * @since 5.4
299 	 */
300 	@SuppressWarnings({ "ReferenceEquality", "StringEquality" })
301 	public static boolean isEnd(String s) {
302 		return s == END;
303 	}
304 
305 	void discardUntilEnd() throws IOException {
306 		for (;;) {
307 			int n = readLength();
308 			if (n == 0) {
309 				break;
310 			}
311 			IO.skipFully(in, n - 4);
312 		}
313 	}
314 
315 	int readLength() throws IOException {
316 		IO.readFully(in, lineBuffer, 0, 4);
317 		int len;
318 		try {
319 			len = RawParseUtils.parseHexInt16(lineBuffer, 0);
320 		} catch (ArrayIndexOutOfBoundsException err) {
321 			throw invalidHeader(err);
322 		}
323 
324 		if (len == 0) {
325 			return 0;
326 		} else if (len == 1) {
327 			return 1;
328 		} else if (len < 4) {
329 			throw invalidHeader();
330 		}
331 
332 		if (limit != 0) {
333 			int n = len - 4;
334 			if (limit < n) {
335 				limit = -1;
336 				try {
337 					IO.skipFully(in, n);
338 				} catch (IOException e) {
339 					// Ignore failure discarding packet over limit.
340 				}
341 				throw new InputOverLimitIOException();
342 			}
343 			// if set limit must not be 0 (means unlimited).
344 			limit = n < limit ? limit - n : -1;
345 		}
346 		return len;
347 	}
348 
349 	private IOException invalidHeader() {
350 		return new IOException(MessageFormat.format(JGitText.get().invalidPacketLineHeader,
351 				"" + (char) lineBuffer[0] + (char) lineBuffer[1] //$NON-NLS-1$
352 				+ (char) lineBuffer[2] + (char) lineBuffer[3]));
353 	}
354 
355 	private IOException invalidHeader(Throwable cause) {
356 		IOException ioe = invalidHeader();
357 		ioe.initCause(cause);
358 		return ioe;
359 	}
360 
361 	/**
362 	 * IOException thrown by read when the configured input limit is exceeded.
363 	 *
364 	 * @since 4.7
365 	 */
366 	public static class InputOverLimitIOException extends IOException {
367 		private static final long serialVersionUID = 1L;
368 	}
369 
370 	/**
371 	 * Iterator over packet lines.
372 	 * <p>
373 	 * Calls {@link #readString()} on the {@link PacketLineIn} until
374 	 * {@link #END} is encountered.
375 	 *
376 	 * @since 5.4
377 	 *
378 	 */
379 	public static class PacketLineInIterator implements Iterable<String> {
380 		private PacketLineIn in;
381 
382 		private String current;
383 
384 		PacketLineInIterator(PacketLineIn in) throws IOException {
385 			this.in = in;
386 			current = in.readString();
387 		}
388 
389 		@Override
390 		public Iterator<String> iterator() {
391 			return new Iterator<String>() {
392 				@Override
393 				public boolean hasNext() {
394 					return !PacketLineIn.isEnd(current);
395 				}
396 
397 				@Override
398 				public String next() {
399 					String next = current;
400 					try {
401 						current = in.readString();
402 					} catch (IOException e) {
403 						throw new UncheckedIOException(e);
404 					}
405 					return next;
406 				}
407 			};
408 		}
409 
410 	}
411 }