View Javadoc
1   /*
2    * Copyright (C) 2008-2009, Google Inc.
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.util;
13  
14  import java.io.BufferedOutputStream;
15  import java.io.File;
16  import java.io.FileInputStream;
17  import java.io.FileOutputStream;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.OutputStream;
21  import java.util.ArrayList;
22  
23  import org.eclipse.jgit.internal.JGitText;
24  import org.eclipse.jgit.lib.NullProgressMonitor;
25  import org.eclipse.jgit.lib.ProgressMonitor;
26  
27  /**
28   * A fully buffered output stream.
29   * <p>
30   * Subclasses determine the behavior when the in-memory buffer capacity has been
31   * exceeded and additional bytes are still being received for output.
32   */
33  public abstract class TemporaryBuffer extends OutputStream {
34  	/** Default limit for in-core storage. */
35  	protected static final int DEFAULT_IN_CORE_LIMIT = 1024 * 1024;
36  
37  	/** Chain of data, if we are still completely in-core; otherwise null. */
38  	ArrayList<Block> blocks;
39  
40  	/**
41  	 * Maximum number of bytes we will permit storing in memory.
42  	 * <p>
43  	 * When this limit is reached the data will be shifted to a file on disk,
44  	 * preventing the JVM heap from growing out of control.
45  	 */
46  	private int inCoreLimit;
47  
48  	/** Initial size of block list. */
49  	private int initialBlocks;
50  
51  	/** If {@link #inCoreLimit} has been reached, remainder goes here. */
52  	private OutputStream overflow;
53  
54  	/**
55  	 * Create a new empty temporary buffer.
56  	 *
57  	 * @param limit
58  	 *            maximum number of bytes to store in memory before entering the
59  	 *            overflow output path; also used as the estimated size.
60  	 */
61  	protected TemporaryBuffer(int limit) {
62  		this(limit, limit);
63  	}
64  
65  	/**
66  	 * Create a new empty temporary buffer.
67  	 *
68  	 * @param estimatedSize
69  	 *            estimated size of storage used, to size the initial list of
70  	 *            block pointers.
71  	 * @param limit
72  	 *            maximum number of bytes to store in memory before entering the
73  	 *            overflow output path.
74  	 * @since 4.0
75  	 */
76  	protected TemporaryBuffer(int estimatedSize, int limit) {
77  		if (estimatedSize > limit)
78  			throw new IllegalArgumentException();
79  		this.inCoreLimit = limit;
80  		this.initialBlocks = (estimatedSize - 1) / Block.SZ + 1;
81  		reset();
82  	}
83  
84  	/** {@inheritDoc} */
85  	@Override
86  	public void write(int b) throws IOException {
87  		if (overflow != null) {
88  			overflow.write(b);
89  			return;
90  		}
91  
92  		Block s = last();
93  		if (s.isFull()) {
94  			if (reachedInCoreLimit()) {
95  				overflow.write(b);
96  				return;
97  			}
98  
99  			s = new Block();
100 			blocks.add(s);
101 		}
102 		s.buffer[s.count++] = (byte) b;
103 	}
104 
105 	/** {@inheritDoc} */
106 	@Override
107 	public void write(byte[] b, int off, int len) throws IOException {
108 		if (overflow == null) {
109 			while (len > 0) {
110 				Block s = last();
111 				if (s.isFull()) {
112 					if (reachedInCoreLimit())
113 						break;
114 
115 					s = new Block();
116 					blocks.add(s);
117 				}
118 
119 				final int n = Math.min(s.buffer.length - s.count, len);
120 				System.arraycopy(b, off, s.buffer, s.count, n);
121 				s.count += n;
122 				len -= n;
123 				off += n;
124 			}
125 		}
126 
127 		if (len > 0)
128 			overflow.write(b, off, len);
129 	}
130 
131 	/**
132 	 * Dumps the entire buffer into the overflow stream, and flushes it.
133 	 *
134 	 * @throws java.io.IOException
135 	 *             the overflow stream cannot be started, or the buffer contents
136 	 *             cannot be written to it, or it failed to flush.
137 	 */
138 	protected void doFlush() throws IOException {
139 		if (overflow == null)
140 			switchToOverflow();
141 		overflow.flush();
142 	}
143 
144 	/**
145 	 * Copy all bytes remaining on the input stream into this buffer.
146 	 *
147 	 * @param in
148 	 *            the stream to read from, until EOF is reached.
149 	 * @throws java.io.IOException
150 	 *             an error occurred reading from the input stream, or while
151 	 *             writing to a local temporary file.
152 	 */
153 	public void copy(InputStream in) throws IOException {
154 		if (blocks != null) {
155 			for (;;) {
156 				Block s = last();
157 				if (s.isFull()) {
158 					if (reachedInCoreLimit())
159 						break;
160 					s = new Block();
161 					blocks.add(s);
162 				}
163 
164 				int n = in.read(s.buffer, s.count, s.buffer.length - s.count);
165 				if (n < 1)
166 					return;
167 				s.count += n;
168 			}
169 		}
170 
171 		final byte[] tmp = new byte[Block.SZ];
172 		int n;
173 		while ((n = in.read(tmp)) > 0)
174 			overflow.write(tmp, 0, n);
175 	}
176 
177 	/**
178 	 * Obtain the length (in bytes) of the buffer.
179 	 * <p>
180 	 * The length is only accurate after {@link #close()} has been invoked.
181 	 *
182 	 * @return total length of the buffer, in bytes.
183 	 */
184 	public long length() {
185 		return inCoreLength();
186 	}
187 
188 	private long inCoreLength() {
189 		final Block last = last();
190 		return ((long) blocks.size() - 1) * Block.SZ + last.count;
191 	}
192 
193 	/**
194 	 * Convert this buffer's contents into a contiguous byte array.
195 	 * <p>
196 	 * The buffer is only complete after {@link #close()} has been invoked.
197 	 *
198 	 * @return the complete byte array; length matches {@link #length()}.
199 	 * @throws java.io.IOException
200 	 *             an error occurred reading from a local temporary file
201 	 */
202 	public byte[] toByteArray() throws IOException {
203 		final long len = length();
204 		if (Integer.MAX_VALUE < len)
205 			throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
206 		final byte[] out = new byte[(int) len];
207 		int outPtr = 0;
208 		for (Block b : blocks) {
209 			System.arraycopy(b.buffer, 0, out, outPtr, b.count);
210 			outPtr += b.count;
211 		}
212 		return out;
213 	}
214 
215 	/**
216 	 * Convert this buffer's contents into a contiguous byte array. If this size
217 	 * of the buffer exceeds the limit only return the first {@code limit} bytes
218 	 * <p>
219 	 * The buffer is only complete after {@link #close()} has been invoked.
220 	 *
221 	 * @param limit
222 	 *            the maximum number of bytes to be returned
223 	 * @return the byte array limited to {@code limit} bytes.
224 	 * @throws java.io.IOException
225 	 *             an error occurred reading from a local temporary file
226 	 * @since 4.2
227 	 */
228 	public byte[] toByteArray(int limit) throws IOException {
229 		final long len = Math.min(length(), limit);
230 		if (Integer.MAX_VALUE < len)
231 			throw new OutOfMemoryError(
232 					JGitText.get().lengthExceedsMaximumArraySize);
233 		int length = (int) len;
234 		final byte[] out = new byte[length];
235 		int outPtr = 0;
236 		for (Block b : blocks) {
237 			int toCopy = Math.min(length - outPtr, b.count);
238 			System.arraycopy(b.buffer, 0, out, outPtr, toCopy);
239 			outPtr += toCopy;
240 			if (outPtr == length) {
241 				break;
242 			}
243 		}
244 		return out;
245 	}
246 
247 	/**
248 	 * Send this buffer to an output stream.
249 	 * <p>
250 	 * This method may only be invoked after {@link #close()} has completed
251 	 * normally, to ensure all data is completely transferred.
252 	 *
253 	 * @param os
254 	 *            stream to send this buffer's complete content to.
255 	 * @param pm
256 	 *            if not null progress updates are sent here. Caller should
257 	 *            initialize the task and the number of work units to <code>
258 	 *            {@link #length()}/1024</code>.
259 	 * @throws java.io.IOException
260 	 *             an error occurred reading from a temporary file on the local
261 	 *             system, or writing to the output stream.
262 	 */
263 	public void writeTo(OutputStream os, ProgressMonitor pm)
264 			throws IOException {
265 		if (pm == null)
266 			pm = NullProgressMonitor.INSTANCE;
267 		for (Block b : blocks) {
268 			os.write(b.buffer, 0, b.count);
269 			pm.update(b.count / 1024);
270 		}
271 	}
272 
273 	/**
274 	 * Open an input stream to read from the buffered data.
275 	 * <p>
276 	 * This method may only be invoked after {@link #close()} has completed
277 	 * normally, to ensure all data is completely transferred.
278 	 *
279 	 * @return a stream to read from the buffer. The caller must close the
280 	 *         stream when it is no longer useful.
281 	 * @throws java.io.IOException
282 	 *             an error occurred opening the temporary file.
283 	 */
284 	public InputStream openInputStream() throws IOException {
285 		return new BlockInputStream();
286 	}
287 
288 	/**
289 	 * Same as {@link #openInputStream()} but handling destruction of any
290 	 * associated resources automatically when closing the returned stream.
291 	 *
292 	 * @return an InputStream which will automatically destroy any associated
293 	 *         temporary file on {@link #close()}
294 	 * @throws IOException
295 	 *             in case of an error.
296 	 * @since 4.11
297 	 */
298 	public InputStream openInputStreamWithAutoDestroy() throws IOException {
299 		return new BlockInputStream() {
300 			@Override
301 			public void close() throws IOException {
302 				super.close();
303 				destroy();
304 			}
305 		};
306 	}
307 
308 	/**
309 	 * Reset this buffer for reuse, purging all buffered content.
310 	 */
311 	public void reset() {
312 		if (overflow != null) {
313 			destroy();
314 		}
315 		if (blocks != null)
316 			blocks.clear();
317 		else
318 			blocks = new ArrayList<>(initialBlocks);
319 		blocks.add(new Block(Math.min(inCoreLimit, Block.SZ)));
320 	}
321 
322 	/**
323 	 * Open the overflow output stream, so the remaining output can be stored.
324 	 *
325 	 * @return the output stream to receive the buffered content, followed by
326 	 *         the remaining output.
327 	 * @throws java.io.IOException
328 	 *             the buffer cannot create the overflow stream.
329 	 */
330 	protected abstract OutputStream overflow() throws IOException;
331 
332 	private Block last() {
333 		return blocks.get(blocks.size() - 1);
334 	}
335 
336 	private boolean reachedInCoreLimit() throws IOException {
337 		if (inCoreLength() < inCoreLimit)
338 			return false;
339 
340 		switchToOverflow();
341 		return true;
342 	}
343 
344 	private void switchToOverflow() throws IOException {
345 		overflow = overflow();
346 
347 		final Block last = blocks.remove(blocks.size() - 1);
348 		for (Block b : blocks)
349 			overflow.write(b.buffer, 0, b.count);
350 		blocks = null;
351 
352 		overflow = new BufferedOutputStream(overflow, Block.SZ);
353 		overflow.write(last.buffer, 0, last.count);
354 	}
355 
356 	/** {@inheritDoc} */
357 	@Override
358 	public void close() throws IOException {
359 		if (overflow != null) {
360 			try {
361 				overflow.close();
362 			} finally {
363 				overflow = null;
364 			}
365 		}
366 	}
367 
368 	/**
369 	 * Clear this buffer so it has no data, and cannot be used again.
370 	 */
371 	public void destroy() {
372 		blocks = null;
373 
374 		if (overflow != null) {
375 			try {
376 				overflow.close();
377 			} catch (IOException err) {
378 				// We shouldn't encounter an error closing the file.
379 			} finally {
380 				overflow = null;
381 			}
382 		}
383 	}
384 
385 	/**
386 	 * A fully buffered output stream using local disk storage for large data.
387 	 * <p>
388 	 * Initially this output stream buffers to memory and is therefore similar
389 	 * to ByteArrayOutputStream, but it shifts to using an on disk temporary
390 	 * file if the output gets too large.
391 	 * <p>
392 	 * The content of this buffered stream may be sent to another OutputStream
393 	 * only after this stream has been properly closed by {@link #close()}.
394 	 */
395 	public static class LocalFile extends TemporaryBuffer {
396 		/** Directory to store the temporary file under. */
397 		private final File directory;
398 
399 		/**
400 		 * Location of our temporary file if we are on disk; otherwise null.
401 		 * <p>
402 		 * If we exceeded the {@link #inCoreLimit} we nulled out {@link #blocks}
403 		 * and created this file instead. All output goes here through
404 		 * {@link #overflow}.
405 		 */
406 		private File onDiskFile;
407 
408 		/**
409 		 * Create a new temporary buffer, limiting memory usage.
410 		 *
411 		 * @param directory
412 		 *            if the buffer has to spill over into a temporary file, the
413 		 *            directory where the file should be saved. If null the
414 		 *            system default temporary directory (for example /tmp) will
415 		 *            be used instead.
416 		 */
417 		public LocalFile(File directory) {
418 			this(directory, DEFAULT_IN_CORE_LIMIT);
419 		}
420 
421 		/**
422 		 * Create a new temporary buffer, limiting memory usage.
423 		 *
424 		 * @param directory
425 		 *            if the buffer has to spill over into a temporary file, the
426 		 *            directory where the file should be saved. If null the
427 		 *            system default temporary directory (for example /tmp) will
428 		 *            be used instead.
429 		 * @param inCoreLimit
430 		 *            maximum number of bytes to store in memory. Storage beyond
431 		 *            this limit will use the local file.
432 		 */
433 		public LocalFile(File directory, int inCoreLimit) {
434 			super(inCoreLimit);
435 			this.directory = directory;
436 		}
437 
438 		@Override
439 		protected OutputStream overflow() throws IOException {
440 			onDiskFile = File.createTempFile("jgit_", ".buf", directory); //$NON-NLS-1$ //$NON-NLS-2$
441 			return new BufferedOutputStream(new FileOutputStream(onDiskFile));
442 		}
443 
444 		@Override
445 		public long length() {
446 			if (onDiskFile == null) {
447 				return super.length();
448 			}
449 			return onDiskFile.length();
450 		}
451 
452 		@Override
453 		public byte[] toByteArray() throws IOException {
454 			if (onDiskFile == null) {
455 				return super.toByteArray();
456 			}
457 
458 			final long len = length();
459 			if (Integer.MAX_VALUE < len)
460 				throw new OutOfMemoryError(JGitText.get().lengthExceedsMaximumArraySize);
461 			final byte[] out = new byte[(int) len];
462 			try (FileInputStream in = new FileInputStream(onDiskFile)) {
463 				IO.readFully(in, out, 0, (int) len);
464 			}
465 			return out;
466 		}
467 
468 		@Override
469 		public byte[] toByteArray(int limit) throws IOException {
470 			if (onDiskFile == null) {
471 				return super.toByteArray(limit);
472 			}
473 			final long len = Math.min(length(), limit);
474 			if (Integer.MAX_VALUE < len) {
475 				throw new OutOfMemoryError(
476 						JGitText.get().lengthExceedsMaximumArraySize);
477 			}
478 			final byte[] out = new byte[(int) len];
479 			try (FileInputStream in = new FileInputStream(onDiskFile)) {
480 				int read = 0;
481 				int chunk;
482 				while ((chunk = in.read(out, read, out.length - read)) >= 0) {
483 					read += chunk;
484 					if (read == out.length) {
485 						break;
486 					}
487 				}
488 			}
489 			return out;
490 		}
491 
492 		@Override
493 		public void writeTo(OutputStream os, ProgressMonitor pm)
494 				throws IOException {
495 			if (onDiskFile == null) {
496 				super.writeTo(os, pm);
497 				return;
498 			}
499 			if (pm == null)
500 				pm = NullProgressMonitor.INSTANCE;
501 			try (FileInputStream in = new FileInputStream(onDiskFile)) {
502 				int cnt;
503 				final byte[] buf = new byte[Block.SZ];
504 				while ((cnt = in.read(buf)) >= 0) {
505 					os.write(buf, 0, cnt);
506 					pm.update(cnt / 1024);
507 				}
508 			}
509 		}
510 
511 		@Override
512 		public InputStream openInputStream() throws IOException {
513 			if (onDiskFile == null)
514 				return super.openInputStream();
515 			return new FileInputStream(onDiskFile);
516 		}
517 
518 		@Override
519 		public InputStream openInputStreamWithAutoDestroy() throws IOException {
520 			if (onDiskFile == null) {
521 				return super.openInputStreamWithAutoDestroy();
522 			}
523 			return new FileInputStream(onDiskFile) {
524 				@Override
525 				public void close() throws IOException {
526 					super.close();
527 					destroy();
528 				}
529 			};
530 		}
531 
532 		@Override
533 		public void destroy() {
534 			super.destroy();
535 
536 			if (onDiskFile != null) {
537 				try {
538 					if (!onDiskFile.delete())
539 						onDiskFile.deleteOnExit();
540 				} finally {
541 					onDiskFile = null;
542 				}
543 			}
544 		}
545 	}
546 
547 	/**
548 	 * A temporary buffer that will never exceed its in-memory limit.
549 	 * <p>
550 	 * If the in-memory limit is reached an IOException is thrown, rather than
551 	 * attempting to spool to local disk.
552 	 */
553 	public static class Heap extends TemporaryBuffer {
554 		/**
555 		 * Create a new heap buffer with a maximum storage limit.
556 		 *
557 		 * @param limit
558 		 *            maximum number of bytes that can be stored in this buffer;
559 		 *            also used as the estimated size. Storing beyond this many
560 		 *            will cause an IOException to be thrown during write.
561 		 */
562 		public Heap(int limit) {
563 			super(limit);
564 		}
565 
566 		/**
567 		 * Create a new heap buffer with a maximum storage limit.
568 		 *
569 		 * @param estimatedSize
570 		 *            estimated size of storage used, to size the initial list of
571 		 *            block pointers.
572 		 * @param limit
573 		 *            maximum number of bytes that can be stored in this buffer.
574 		 *            Storing beyond this many will cause an IOException to be
575 		 *            thrown during write.
576 		 * @since 4.0
577 		 */
578 		public Heap(int estimatedSize, int limit) {
579 			super(estimatedSize, limit);
580 		}
581 
582 		@Override
583 		protected OutputStream overflow() throws IOException {
584 			throw new IOException(JGitText.get().inMemoryBufferLimitExceeded);
585 		}
586 	}
587 
588 	static class Block {
589 		static final int SZ = 8 * 1024;
590 
591 		final byte[] buffer;
592 
593 		int count;
594 
595 		Block() {
596 			buffer = new byte[SZ];
597 		}
598 
599 		Block(int sz) {
600 			buffer = new byte[sz];
601 		}
602 
603 		boolean isFull() {
604 			return count == buffer.length;
605 		}
606 	}
607 
608 	private class BlockInputStream extends InputStream {
609 		private byte[] singleByteBuffer;
610 		private int blockIndex;
611 		private Block block;
612 		private int blockPos;
613 
614 		BlockInputStream() {
615 			block = blocks.get(blockIndex);
616 		}
617 
618 		@Override
619 		public int read() throws IOException {
620 			if (singleByteBuffer == null)
621 				singleByteBuffer = new byte[1];
622 			int n = read(singleByteBuffer);
623 			return n == 1 ? singleByteBuffer[0] & 0xff : -1;
624 		}
625 
626 		@Override
627 		public long skip(long cnt) throws IOException {
628 			long skipped = 0;
629 			while (0 < cnt) {
630 				int n = (int) Math.min(block.count - blockPos, cnt);
631 				if (0 < n) {
632 					blockPos += n;
633 					skipped += n;
634 					cnt -= n;
635 				} else if (nextBlock())
636 					continue;
637 				else
638 					break;
639 			}
640 			return skipped;
641 		}
642 
643 		@Override
644 		public int read(byte[] b, int off, int len) throws IOException {
645 			if (len == 0)
646 				return 0;
647 			int copied = 0;
648 			while (0 < len) {
649 				int c = Math.min(block.count - blockPos, len);
650 				if (0 < c) {
651 					System.arraycopy(block.buffer, blockPos, b, off, c);
652 					blockPos += c;
653 					off += c;
654 					len -= c;
655 					copied += c;
656 				} else if (nextBlock())
657 					continue;
658 				else
659 					break;
660 			}
661 			return 0 < copied ? copied : -1;
662 		}
663 
664 		private boolean nextBlock() {
665 			if (++blockIndex < blocks.size()) {
666 				block = blocks.get(blockIndex);
667 				blockPos = 0;
668 				return true;
669 			}
670 			return false;
671 		}
672 	}
673 }