View Javadoc
1   /*
2    * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>
3    * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> 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.util.io;
13  
14  import java.io.IOException;
15  import java.io.OutputStream;
16  
17  import org.eclipse.jgit.diff.RawText;
18  
19  /**
20   * An OutputStream that reduces CRLF to LF.
21   * <p>
22   * Existing single CR are not changed to LF, but retained as is.
23   * </p>
24   * <p>
25   * A binary check on the first 8000 bytes is performed and in case of binary
26   * files, canonicalization is turned off (for the complete file). If the binary
27   * check determines that the input is not binary but text with CR/LF,
28   * canonicalization is also turned off.
29   * </p>
30   *
31   * @since 4.3
32   */
33  public class AutoLFOutputStream extends OutputStream {
34  
35  	static final int BUFFER_SIZE = 8000;
36  
37  	private final OutputStream out;
38  
39  	private int buf = -1;
40  
41  	private byte[] binbuf = new byte[BUFFER_SIZE];
42  
43  	private byte[] onebytebuf = new byte[1];
44  
45  	private int binbufcnt = 0;
46  
47  	private boolean detectBinary;
48  
49  	private boolean isBinary;
50  
51  	/**
52  	 * Constructor for AutoLFOutputStream.
53  	 *
54  	 * @param out
55  	 *            an {@link java.io.OutputStream} object.
56  	 */
57  	public AutoLFOutputStream(OutputStream out) {
58  		this(out, true);
59  	}
60  
61  	/**
62  	 * Constructor for AutoLFOutputStream.
63  	 *
64  	 * @param out
65  	 *            an {@link java.io.OutputStream} object.
66  	 * @param detectBinary
67  	 *            whether binaries should be detected
68  	 */
69  	public AutoLFOutputStream(OutputStream out, boolean detectBinary) {
70  		this.out = out;
71  		this.detectBinary = detectBinary;
72  	}
73  
74  	/** {@inheritDoc} */
75  	@Override
76  	public void write(int b) throws IOException {
77  		onebytebuf[0] = (byte) b;
78  		write(onebytebuf, 0, 1);
79  	}
80  
81  	/** {@inheritDoc} */
82  	@Override
83  	public void write(byte[] b) throws IOException {
84  		int overflow = buffer(b, 0, b.length);
85  		if (overflow > 0) {
86  			write(b, b.length - overflow, overflow);
87  		}
88  	}
89  
90  	/** {@inheritDoc} */
91  	@Override
92  	public void write(byte[] b, int startOff, int startLen)
93  			throws IOException {
94  		final int overflow = buffer(b, startOff, startLen);
95  		if (overflow <= 0) {
96  			return;
97  		}
98  		final int off = startOff + startLen - overflow;
99  		final int len = overflow;
100 		int lastw = off;
101 		if (isBinary) {
102 			out.write(b, off, len);
103 			return;
104 		}
105 		for (int i = off; i < off + len; ++i) {
106 			final byte c = b[i];
107 			switch (c) {
108 			case '\r':
109 				// skip write r but backlog r
110 				if (lastw < i) {
111 					out.write(b, lastw, i - lastw);
112 				}
113 				lastw = i + 1;
114 				buf = '\r';
115 				break;
116 			case '\n':
117 				if (buf == '\r') {
118 					out.write('\n');
119 					lastw = i + 1;
120 					buf = -1;
121 				} else {
122 					if (lastw < i + 1) {
123 						out.write(b, lastw, i + 1 - lastw);
124 					}
125 					lastw = i + 1;
126 				}
127 				break;
128 			default:
129 				if (buf == '\r') {
130 					out.write('\r');
131 					lastw = i;
132 				}
133 				buf = -1;
134 				break;
135 			}
136 		}
137 		if (lastw < off + len) {
138 			out.write(b, lastw, off + len - lastw);
139 		}
140 	}
141 
142 	private int buffer(byte[] b, int off, int len) throws IOException {
143 		if (binbufcnt > binbuf.length) {
144 			return len;
145 		}
146 		int copy = Math.min(binbuf.length - binbufcnt, len);
147 		System.arraycopy(b, off, binbuf, binbufcnt, copy);
148 		binbufcnt += copy;
149 		int remaining = len - copy;
150 		if (remaining > 0) {
151 			decideMode();
152 		}
153 		return remaining;
154 	}
155 
156 	private void decideMode() throws IOException {
157 		if (detectBinary) {
158 			isBinary = RawText.isBinary(binbuf, binbufcnt);
159 			if (!isBinary) {
160 				isBinary = RawText.isCrLfText(binbuf, binbufcnt);
161 			}
162 			detectBinary = false;
163 		}
164 		int cachedLen = binbufcnt;
165 		binbufcnt = binbuf.length + 1; // full!
166 		write(binbuf, 0, cachedLen);
167 	}
168 
169 	/** {@inheritDoc} */
170 	@Override
171 	public void flush() throws IOException {
172 		if (binbufcnt <= binbuf.length) {
173 			decideMode();
174 		}
175 		out.flush();
176 	}
177 
178 	/** {@inheritDoc} */
179 	@Override
180 	public void close() throws IOException {
181 		flush();
182 		if (buf == '\r') {
183 			out.write(buf);
184 			buf = -1;
185 		}
186 		out.close();
187 	}
188 }