View Javadoc
1   /*
2    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  
12  package org.eclipse.jgit.transport;
13  
14  import static java.nio.charset.StandardCharsets.UTF_8;
15  import static org.eclipse.jgit.transport.SideBandOutputStream.HDR_SIZE;
16  
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.OutputStream;
20  import java.io.Writer;
21  import java.text.MessageFormat;
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  
25  import org.eclipse.jgit.errors.PackProtocolException;
26  import org.eclipse.jgit.errors.TransportException;
27  import org.eclipse.jgit.internal.JGitText;
28  import org.eclipse.jgit.lib.ProgressMonitor;
29  import org.eclipse.jgit.util.IO;
30  import org.eclipse.jgit.util.RawParseUtils;
31  
32  /**
33   * Unmultiplexes the data portion of a side-band channel.
34   * <p>
35   * Reading from this input stream obtains data from channel 1, which is
36   * typically the bulk data stream.
37   * <p>
38   * Channel 2 is transparently unpacked and "scraped" to update a progress
39   * monitor. The scraping is performed behind the scenes as part of any of the
40   * read methods offered by this stream.
41   * <p>
42   * Channel 3 results in an exception being thrown, as the remote side has issued
43   * an unrecoverable error.
44   *
45   * @see SideBandOutputStream
46   * @since 4.11
47   */
48  public class SideBandInputStream extends InputStream {
49  	static final int CH_DATA = 1;
50  	static final int CH_PROGRESS = 2;
51  	static final int CH_ERROR = 3;
52  
53  	private static Pattern P_UNBOUNDED = Pattern
54  			.compile("^([\\w ]+): +(\\d+)(?:, done\\.)? *[\r\n]$"); //$NON-NLS-1$
55  
56  	private static Pattern P_BOUNDED = Pattern
57  			.compile("^([\\w ]+): +\\d+% +\\( *(\\d+)/ *(\\d+)\\)(?:, done\\.)? *[\r\n]$"); //$NON-NLS-1$
58  
59  	private final InputStream rawIn;
60  
61  	private final PacketLineIn pckIn;
62  
63  	private final ProgressMonitor monitor;
64  
65  	private final Writer messages;
66  
67  	private final OutputStream out;
68  
69  	private String progressBuffer = ""; //$NON-NLS-1$
70  
71  	private String currentTask;
72  
73  	private int lastCnt;
74  
75  	private boolean eof;
76  
77  	private int channel;
78  
79  	private int available;
80  
81  	SideBandInputStream(final InputStream in, final ProgressMonitor progress,
82  			final Writer messageStream, OutputStream outputStream) {
83  		rawIn = in;
84  		pckIn = new PacketLineIn(rawIn);
85  		monitor = progress;
86  		messages = messageStream;
87  		currentTask = ""; //$NON-NLS-1$
88  		out = outputStream;
89  	}
90  
91  	/** {@inheritDoc} */
92  	@Override
93  	public int read() throws IOException {
94  		needDataPacket();
95  		if (eof)
96  			return -1;
97  		available--;
98  		return rawIn.read();
99  	}
100 
101 	/** {@inheritDoc} */
102 	@Override
103 	public int read(byte[] b, int off, int len) throws IOException {
104 		int r = 0;
105 		while (len > 0) {
106 			needDataPacket();
107 			if (eof)
108 				break;
109 			final int n = rawIn.read(b, off, Math.min(len, available));
110 			if (n < 0)
111 				break;
112 			r += n;
113 			off += n;
114 			len -= n;
115 			available -= n;
116 		}
117 		return eof && r == 0 ? -1 : r;
118 	}
119 
120 	private void needDataPacket() throws IOException {
121 		if (eof || (channel == CH_DATA && available > 0))
122 			return;
123 		for (;;) {
124 			available = pckIn.readLength();
125 			if (available == 0) {
126 				eof = true;
127 				return;
128 			}
129 
130 			channel = rawIn.read() & 0xff;
131 			available -= HDR_SIZE; // length header plus channel indicator
132 			if (available == 0)
133 				continue;
134 
135 			switch (channel) {
136 			case CH_DATA:
137 				return;
138 			case CH_PROGRESS:
139 				progress(readString(available));
140 				continue;
141 			case CH_ERROR:
142 				eof = true;
143 				throw new TransportException(remote(readString(available)));
144 			default:
145 				throw new PackProtocolException(
146 						MessageFormat.format(JGitText.get().invalidChannel,
147 								Integer.valueOf(channel)));
148 			}
149 		}
150 	}
151 
152 	private void progress(String pkt) throws IOException {
153 		pkt = progressBuffer + pkt;
154 		for (;;) {
155 			final int lf = pkt.indexOf('\n');
156 			final int cr = pkt.indexOf('\r');
157 			final int s;
158 			if (0 <= lf && 0 <= cr)
159 				s = Math.min(lf, cr);
160 			else if (0 <= lf)
161 				s = lf;
162 			else if (0 <= cr)
163 				s = cr;
164 			else
165 				break;
166 
167 			doProgressLine(pkt.substring(0, s + 1));
168 			pkt = pkt.substring(s + 1);
169 		}
170 		progressBuffer = pkt;
171 	}
172 
173 	private void doProgressLine(String msg) throws IOException {
174 		Matcher matcher;
175 
176 		matcher = P_BOUNDED.matcher(msg);
177 		if (matcher.matches()) {
178 			final String taskname = matcher.group(1);
179 			if (!currentTask.equals(taskname)) {
180 				currentTask = taskname;
181 				lastCnt = 0;
182 				beginTask(Integer.parseInt(matcher.group(3)));
183 			}
184 			final int cnt = Integer.parseInt(matcher.group(2));
185 			monitor.update(cnt - lastCnt);
186 			lastCnt = cnt;
187 			return;
188 		}
189 
190 		matcher = P_UNBOUNDED.matcher(msg);
191 		if (matcher.matches()) {
192 			final String taskname = matcher.group(1);
193 			if (!currentTask.equals(taskname)) {
194 				currentTask = taskname;
195 				lastCnt = 0;
196 				beginTask(ProgressMonitor.UNKNOWN);
197 			}
198 			final int cnt = Integer.parseInt(matcher.group(2));
199 			monitor.update(cnt - lastCnt);
200 			lastCnt = cnt;
201 			return;
202 		}
203 
204 		messages.write(msg);
205 		if (out != null)
206 			out.write(msg.getBytes(UTF_8));
207 	}
208 
209 	private void beginTask(int totalWorkUnits) {
210 		monitor.beginTask(remote(currentTask), totalWorkUnits);
211 	}
212 
213 	private static String remote(String msg) {
214 		String prefix = JGitText.get().prefixRemote;
215 		StringBuilder r = new StringBuilder(prefix.length() + msg.length() + 1);
216 		r.append(prefix);
217 		if (prefix.length() > 0 && prefix.charAt(prefix.length() - 1) != ' ') {
218 			r.append(' ');
219 		}
220 		r.append(msg);
221 		return r.toString();
222 	}
223 
224 	private String readString(int len) throws IOException {
225 		final byte[] raw = new byte[len];
226 		IO.readFully(rawIn, raw, 0, len);
227 		return RawParseUtils.decode(UTF_8, raw, 0, len);
228 	}
229 }