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 	AckNackResult readACK(MutableObjectId returnedId) throws IOException {
107 		final String line = readString();
108 		if (line.length() == 0)
109 			throw new PackProtocolException(JGitText.get().expectedACKNAKFoundEOF);
110 		if ("NAK".equals(line)) //$NON-NLS-1$
111 			return AckNackResult.NAK;
112 		if (line.startsWith("ACK ")) { //$NON-NLS-1$
113 			returnedId.fromString(line.substring(4, 44));
114 			if (line.length() == 44)
115 				return AckNackResult.ACK;
116 
117 			final String arg = line.substring(44);
118 			switch (arg) {
119 			case " continue": //$NON-NLS-1$
120 				return AckNackResult.ACK_CONTINUE;
121 			case " common": //$NON-NLS-1$
122 				return AckNackResult.ACK_COMMON;
123 			case " ready": //$NON-NLS-1$
124 				return AckNackResult.ACK_READY;
125 			default:
126 				break;
127 			}
128 		}
129 		if (line.startsWith("ERR ")) //$NON-NLS-1$
130 			throw new PackProtocolException(line.substring(4));
131 		throw new PackProtocolException(MessageFormat.format(JGitText.get().expectedACKNAKGot, line));
132 	}
133 
134 	/**
135 	 * Read a single UTF-8 encoded string packet from the input stream.
136 	 * <p>
137 	 * If the string ends with an LF, it will be removed before returning the
138 	 * value to the caller. If this automatic trimming behavior is not desired,
139 	 * use {@link #readStringRaw()} instead.
140 	 *
141 	 * @return the string. {@link #END} if the string was the magic flush
142 	 *         packet, {@link #DELIM} if the string was the magic DELIM
143 	 *         packet.
144 	 * @throws java.io.IOException
145 	 *             the stream cannot be read.
146 	 */
147 	public String readString() throws IOException {
148 		int len = readLength();
149 		if (len == 0) {
150 			log.debug("git< 0000"); //$NON-NLS-1$
151 			return END;
152 		}
153 		if (len == 1) {
154 			log.debug("git< 0001"); //$NON-NLS-1$
155 			return DELIM;
156 		}
157 
158 		len -= 4; // length header (4 bytes)
159 		if (len == 0) {
160 			log.debug("git< "); //$NON-NLS-1$
161 			return ""; //$NON-NLS-1$
162 		}
163 
164 		byte[] raw;
165 		if (len <= lineBuffer.length)
166 			raw = lineBuffer;
167 		else
168 			raw = new byte[len];
169 
170 		IO.readFully(in, raw, 0, len);
171 		if (raw[len - 1] == '\n')
172 			len--;
173 
174 		String s = RawParseUtils.decode(UTF_8, raw, 0, len);
175 		log.debug("git< " + s); //$NON-NLS-1$
176 		return s;
177 	}
178 
179 	/**
180 	 * Get an iterator to read strings from the input stream.
181 	 *
182 	 * @return an iterator that calls {@link #readString()} until {@link #END}
183 	 *         is encountered.
184 	 *
185 	 * @throws IOException
186 	 *             on failure to read the initial packet line.
187 	 * @since 5.4
188 	 */
189 	public PacketLineInIterator readStrings() throws IOException {
190 		return new PacketLineInIterator(this);
191 	}
192 
193 	/**
194 	 * Read a single UTF-8 encoded string packet from the input stream.
195 	 * <p>
196 	 * Unlike {@link #readString()} a trailing LF will be retained.
197 	 *
198 	 * @return the string. {@link #END} if the string was the magic flush
199 	 *         packet.
200 	 * @throws java.io.IOException
201 	 *             the stream cannot be read.
202 	 */
203 	public String readStringRaw() throws IOException {
204 		int len = readLength();
205 		if (len == 0) {
206 			log.debug("git< 0000"); //$NON-NLS-1$
207 			return END;
208 		}
209 
210 		len -= 4; // length header (4 bytes)
211 
212 		byte[] raw;
213 		if (len <= lineBuffer.length)
214 			raw = lineBuffer;
215 		else
216 			raw = new byte[len];
217 
218 		IO.readFully(in, raw, 0, len);
219 
220 		String s = RawParseUtils.decode(UTF_8, raw, 0, len);
221 		log.debug("git< " + s); //$NON-NLS-1$
222 		return s;
223 	}
224 
225 	/**
226 	 * Check if a string is the delimiter marker.
227 	 *
228 	 * @param s
229 	 *            the string to check
230 	 * @return true if the given string is {@link #DELIM}, otherwise false.
231 	 * @since 5.4
232 	 */
233 	@SuppressWarnings({ "ReferenceEquality", "StringEquality" })
234 	public static boolean isDelimiter(String s) {
235 		return s == DELIM;
236 	}
237 
238 	/**
239 	 * Get the delimiter marker.
240 	 * <p>
241 	 * Intended for use only in tests.
242 	 *
243 	 * @return The delimiter marker.
244 	 */
245 	static String delimiter() {
246 		return DELIM;
247 	}
248 
249 	/**
250 	 * Get the end marker.
251 	 * <p>
252 	 * Intended for use only in tests.
253 	 *
254 	 * @return The end marker.
255 	 */
256 	static String end() {
257 		return END;
258 	}
259 
260 	/**
261 	 * Check if a string is the packet end marker.
262 	 *
263 	 * @param s
264 	 *            the string to check
265 	 * @return true if the given string is {@link #END}, otherwise false.
266 	 * @since 5.4
267 	 */
268 	@SuppressWarnings({ "ReferenceEquality", "StringEquality" })
269 	public static boolean isEnd(String s) {
270 		return s == END;
271 	}
272 
273 	void discardUntilEnd() throws IOException {
274 		for (;;) {
275 			int n = readLength();
276 			if (n == 0) {
277 				break;
278 			}
279 			IO.skipFully(in, n - 4);
280 		}
281 	}
282 
283 	int readLength() throws IOException {
284 		IO.readFully(in, lineBuffer, 0, 4);
285 		int len;
286 		try {
287 			len = RawParseUtils.parseHexInt16(lineBuffer, 0);
288 		} catch (ArrayIndexOutOfBoundsException err) {
289 			throw invalidHeader(err);
290 		}
291 
292 		if (len == 0) {
293 			return 0;
294 		} else if (len == 1) {
295 			return 1;
296 		} else if (len < 4) {
297 			throw invalidHeader();
298 		}
299 
300 		if (limit != 0) {
301 			int n = len - 4;
302 			if (limit < n) {
303 				limit = -1;
304 				try {
305 					IO.skipFully(in, n);
306 				} catch (IOException e) {
307 					// Ignore failure discarding packet over limit.
308 				}
309 				throw new InputOverLimitIOException();
310 			}
311 			// if set limit must not be 0 (means unlimited).
312 			limit = n < limit ? limit - n : -1;
313 		}
314 		return len;
315 	}
316 
317 	private IOException invalidHeader() {
318 		return new IOException(MessageFormat.format(JGitText.get().invalidPacketLineHeader,
319 				"" + (char) lineBuffer[0] + (char) lineBuffer[1] //$NON-NLS-1$
320 				+ (char) lineBuffer[2] + (char) lineBuffer[3]));
321 	}
322 
323 	private IOException invalidHeader(Throwable cause) {
324 		IOException ioe = invalidHeader();
325 		ioe.initCause(cause);
326 		return ioe;
327 	}
328 
329 	/**
330 	 * IOException thrown by read when the configured input limit is exceeded.
331 	 *
332 	 * @since 4.7
333 	 */
334 	public static class InputOverLimitIOException extends IOException {
335 		private static final long serialVersionUID = 1L;
336 	}
337 
338 	/**
339 	 * Iterator over packet lines.
340 	 * <p>
341 	 * Calls {@link #readString()} on the {@link PacketLineIn} until
342 	 * {@link #END} is encountered.
343 	 *
344 	 * @since 5.4
345 	 *
346 	 */
347 	public static class PacketLineInIterator implements Iterable<String> {
348 		private PacketLineIn in;
349 
350 		private String current;
351 
352 		PacketLineInIterator(PacketLineIn in) throws IOException {
353 			this.in = in;
354 			current = in.readString();
355 		}
356 
357 		@Override
358 		public Iterator<String> iterator() {
359 			return new Iterator<String>() {
360 				@Override
361 				public boolean hasNext() {
362 					return !PacketLineIn.isEnd(current);
363 				}
364 
365 				@Override
366 				public String next() {
367 					String next = current;
368 					try {
369 						current = in.readString();
370 					} catch (IOException e) {
371 						throw new UncheckedIOException(e);
372 					}
373 					return next;
374 				}
375 			};
376 		}
377 
378 	}
379 }