View Javadoc
1   /*
2    * Copyright (C) 2017, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *	 notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *	 copyright notice, this list of conditions and the following
21   *	 disclaimer in the documentation and/or other materials provided
22   *	 with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *	 names of its contributors may be used to endorse or promote
26   *	 products derived from this software without specific prior
27   *	 written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.internal.storage.file;
45  
46  import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
47  import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
48  import static org.eclipse.jgit.lib.Constants.OBJ_OFS_DELTA;
49  import static org.eclipse.jgit.lib.Constants.OBJ_REF_DELTA;
50  
51  import java.io.BufferedInputStream;
52  import java.io.EOFException;
53  import java.io.File;
54  import java.io.FileOutputStream;
55  import java.io.FilterInputStream;
56  import java.io.IOException;
57  import java.io.InputStream;
58  import java.io.OutputStream;
59  import java.io.RandomAccessFile;
60  import java.nio.channels.Channels;
61  import java.text.MessageFormat;
62  import java.util.Collection;
63  import java.util.Collections;
64  import java.util.HashSet;
65  import java.util.List;
66  import java.util.Set;
67  import java.util.zip.CRC32;
68  import java.util.zip.DataFormatException;
69  import java.util.zip.Deflater;
70  import java.util.zip.DeflaterOutputStream;
71  import java.util.zip.Inflater;
72  import java.util.zip.InflaterInputStream;
73  
74  import org.eclipse.jgit.errors.CorruptObjectException;
75  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
76  import org.eclipse.jgit.errors.LargeObjectException;
77  import org.eclipse.jgit.errors.MissingObjectException;
78  import org.eclipse.jgit.internal.JGitText;
79  import org.eclipse.jgit.internal.storage.pack.PackExt;
80  import org.eclipse.jgit.lib.AbbreviatedObjectId;
81  import org.eclipse.jgit.lib.AnyObjectId;
82  import org.eclipse.jgit.lib.Constants;
83  import org.eclipse.jgit.lib.InflaterCache;
84  import org.eclipse.jgit.lib.ObjectId;
85  import org.eclipse.jgit.lib.ObjectIdOwnerMap;
86  import org.eclipse.jgit.lib.ObjectInserter;
87  import org.eclipse.jgit.lib.ObjectLoader;
88  import org.eclipse.jgit.lib.ObjectReader;
89  import org.eclipse.jgit.lib.ObjectStream;
90  import org.eclipse.jgit.storage.pack.PackConfig;
91  import org.eclipse.jgit.transport.PackParser;
92  import org.eclipse.jgit.transport.PackedObjectInfo;
93  import org.eclipse.jgit.util.BlockList;
94  import org.eclipse.jgit.util.FileUtils;
95  import org.eclipse.jgit.util.IO;
96  import org.eclipse.jgit.util.NB;
97  import org.eclipse.jgit.util.io.CountingOutputStream;
98  import org.eclipse.jgit.util.sha1.SHA1;
99  
100 /**
101  * Object inserter that inserts one pack per call to {@link #flush()}, and never
102  * inserts loose objects.
103  */
104 public class PackInserter extends ObjectInserter {
105 	/** Always produce version 2 indexes, to get CRC data. */
106 	private static final int INDEX_VERSION = 2;
107 
108 	private final ObjectDirectory db;
109 
110 	private List<PackedObjectInfo> objectList;
111 	private ObjectIdOwnerMap<PackedObjectInfo> objectMap;
112 	private boolean rollback;
113 	private boolean checkExisting = true;
114 
115 	private int compression = Deflater.BEST_COMPRESSION;
116 	private File tmpPack;
117 	private PackStream packOut;
118 	private Inflater cachedInflater;
119 
120 	private PackConfig pconfig;
121 
122 	PackInserter(ObjectDirectory db) {
123 		this.db = db;
124 		this.pconfig = new PackConfig(db.getConfig());
125 	}
126 
127 	/**
128 	 * Whether to check if objects exist in the repo
129 	 *
130 	 * @param check
131 	 *            if {@code false}, will write out possibly-duplicate objects
132 	 *            without first checking whether they exist in the repo; default
133 	 *            is true.
134 	 */
135 	public void checkExisting(boolean check) {
136 		checkExisting = check;
137 	}
138 
139 	/**
140 	 * Set compression level for zlib deflater.
141 	 *
142 	 * @param compression
143 	 *            compression level for zlib deflater.
144 	 */
145 	public void setCompressionLevel(int compression) {
146 		this.compression = compression;
147 	}
148 
149 	int getBufferSize() {
150 		return buffer().length;
151 	}
152 
153 	/** {@inheritDoc} */
154 	@Override
155 	public ObjectId insert(int type, byte[] data, int off, int len)
156 			throws IOException {
157 		ObjectId id = idFor(type, data, off, len);
158 		if (objectMap != null && objectMap.contains(id)) {
159 			return id;
160 		}
161 		// Ignore loose objects, which are potentially unreachable.
162 		if (checkExisting && db.hasPackedObject(id)) {
163 			return id;
164 		}
165 
166 		long offset = beginObject(type, len);
167 		packOut.compress.write(data, off, len);
168 		packOut.compress.finish();
169 		return endObject(id, offset);
170 	}
171 
172 	/** {@inheritDoc} */
173 	@Override
174 	public ObjectId insert(int type, long len, InputStream in)
175 			throws IOException {
176 		byte[] buf = buffer();
177 		if (len <= buf.length) {
178 			IO.readFully(in, buf, 0, (int) len);
179 			return insert(type, buf, 0, (int) len);
180 		}
181 
182 		long offset = beginObject(type, len);
183 		SHA1 md = digest();
184 		md.update(Constants.encodedTypeString(type));
185 		md.update((byte) ' ');
186 		md.update(Constants.encodeASCII(len));
187 		md.update((byte) 0);
188 
189 		while (0 < len) {
190 			int n = in.read(buf, 0, (int) Math.min(buf.length, len));
191 			if (n <= 0) {
192 				throw new EOFException();
193 			}
194 			md.update(buf, 0, n);
195 			packOut.compress.write(buf, 0, n);
196 			len -= n;
197 		}
198 		packOut.compress.finish();
199 		return endObject(md.toObjectId(), offset);
200 	}
201 
202 	private long beginObject(int type, long len) throws IOException {
203 		if (packOut == null) {
204 			beginPack();
205 		}
206 		long offset = packOut.getOffset();
207 		packOut.beginObject(type, len);
208 		return offset;
209 	}
210 
211 	private ObjectId endObject(ObjectId id, long offset) {
212 		PackedObjectInfo obj = new PackedObjectInfo(id);
213 		obj.setOffset(offset);
214 		obj.setCRC((int) packOut.crc32.getValue());
215 		objectList.add(obj);
216 		objectMap.addIfAbsent(obj);
217 		return id;
218 	}
219 
220 	private static File idxFor(File packFile) {
221 		String p = packFile.getName();
222 		return new File(
223 				packFile.getParentFile(),
224 				p.substring(0, p.lastIndexOf('.')) + ".idx"); //$NON-NLS-1$
225 	}
226 
227 	private void beginPack() throws IOException {
228 		objectList = new BlockList<>();
229 		objectMap = new ObjectIdOwnerMap<>();
230 
231 		rollback = true;
232 		tmpPack = File.createTempFile("insert_", ".pack", db.getDirectory()); //$NON-NLS-1$ //$NON-NLS-2$
233 		packOut = new PackStream(tmpPack);
234 
235 		// Write the header as though it were a single object pack.
236 		packOut.write(packOut.hdrBuf, 0, writePackHeader(packOut.hdrBuf, 1));
237 	}
238 
239 	private static int writePackHeader(byte[] buf, int objectCount) {
240 		System.arraycopy(Constants.PACK_SIGNATURE, 0, buf, 0, 4);
241 		NB.encodeInt32(buf, 4, 2); // Always use pack version 2.
242 		NB.encodeInt32(buf, 8, objectCount);
243 		return 12;
244 	}
245 
246 	/** {@inheritDoc} */
247 	@Override
248 	public PackParser newPackParser(InputStream in) {
249 		throw new UnsupportedOperationException();
250 	}
251 
252 	/** {@inheritDoc} */
253 	@Override
254 	public ObjectReader newReader() {
255 		return new Reader();
256 	}
257 
258 	/** {@inheritDoc} */
259 	@Override
260 	public void flush() throws IOException {
261 		if (tmpPack == null) {
262 			return;
263 		}
264 
265 		if (packOut == null) {
266 			throw new IOException();
267 		}
268 
269 		byte[] packHash;
270 		try {
271 			packHash = packOut.finishPack();
272 		} finally {
273 			packOut = null;
274 		}
275 
276 		Collections.sort(objectList);
277 		File tmpIdx = idxFor(tmpPack); // TODO(nasserg) Use PackFile?
278 		writePackIndex(tmpIdx, packHash, objectList);
279 
280 		PackFile realPack = new PackFile(db.getPackDirectory(),
281 				computeName(objectList), PackExt.PACK);
282 		db.closeAllPackHandles(realPack);
283 		tmpPack.setReadOnly();
284 		FileUtils.rename(tmpPack, realPack, ATOMIC_MOVE);
285 
286 		PackFile realIdx = realPack.create(PackExt.INDEX);
287 		tmpIdx.setReadOnly();
288 		try {
289 			FileUtils.rename(tmpIdx, realIdx, ATOMIC_MOVE);
290 		} catch (IOException e) {
291 			File newIdx = new File(
292 					realIdx.getParentFile(), realIdx.getName() + ".new"); //$NON-NLS-1$
293 			try {
294 				FileUtils.rename(tmpIdx, newIdx, ATOMIC_MOVE);
295 			} catch (IOException e2) {
296 				newIdx = tmpIdx;
297 				e = e2;
298 			}
299 			throw new IOException(MessageFormat.format(
300 					JGitText.get().panicCantRenameIndexFile, newIdx,
301 					realIdx), e);
302 		}
303 
304 		boolean interrupted = false;
305 		try {
306 			FileSnapshot snapshot = FileSnapshot.save(realPack);
307 			if (pconfig.doWaitPreventRacyPack(snapshot.size())) {
308 				snapshot.waitUntilNotRacy();
309 			}
310 		} catch (InterruptedException e) {
311 			interrupted = true;
312 		}
313 		try {
314 			db.openPack(realPack);
315 			rollback = false;
316 		} finally {
317 			clear();
318 			if (interrupted) {
319 				// Re-set interrupted flag
320 				Thread.currentThread().interrupt();
321 			}
322 		}
323 	}
324 
325 	private static void writePackIndex(File idx, byte[] packHash,
326 			List<PackedObjectInfo> list) throws IOException {
327 		try (OutputStream os = new FileOutputStream(idx)) {
328 			PackIndexWriter w = PackIndexWriter.createVersion(os, INDEX_VERSION);
329 			w.write(list, packHash);
330 		}
331 	}
332 
333 	private ObjectId computeName(List<PackedObjectInfo> list) {
334 		SHA1 md = digest().reset();
335 		byte[] buf = buffer();
336 		for (PackedObjectInfo otp : list) {
337 			otp.copyRawTo(buf, 0);
338 			md.update(buf, 0, OBJECT_ID_LENGTH);
339 		}
340 		return ObjectId.fromRaw(md.digest());
341 	}
342 
343 	/** {@inheritDoc} */
344 	@Override
345 	public void close() {
346 		try {
347 			if (packOut != null) {
348 				try {
349 					packOut.close();
350 				} catch (IOException err) {
351 					// Ignore a close failure, the pack should be removed.
352 				}
353 			}
354 			if (rollback && tmpPack != null) {
355 				try {
356 					FileUtils.delete(tmpPack);
357 				} catch (IOException e) {
358 					// Still delete idx.
359 				}
360 				try {
361 					FileUtils.delete(idxFor(tmpPack));
362 				} catch (IOException e) {
363 					// Ignore error deleting temp idx.
364 				}
365 				rollback = false;
366 			}
367 		} finally {
368 			clear();
369 			try {
370 				InflaterCache.release(cachedInflater);
371 			} finally {
372 				cachedInflater = null;
373 			}
374 		}
375 	}
376 
377 	private void clear() {
378 		objectList = null;
379 		objectMap = null;
380 		tmpPack = null;
381 		packOut = null;
382 	}
383 
384 	private Inflater inflater() {
385 		if (cachedInflater == null) {
386 			cachedInflater = InflaterCache.get();
387 		} else {
388 			cachedInflater.reset();
389 		}
390 		return cachedInflater;
391 	}
392 
393 	/**
394 	 * Stream that writes to a pack file.
395 	 * <p>
396 	 * Backed by two views of the same open file descriptor: a random-access file,
397 	 * and an output stream. Seeking in the file causes subsequent writes to the
398 	 * output stream to occur wherever the file pointer is pointing, so we need to
399 	 * take care to always seek to the end of the file before writing a new
400 	 * object.
401 	 * <p>
402 	 * Callers should always use {@link #seek(long)} to seek, rather than reaching
403 	 * into the file member. As long as this contract is followed, calls to {@link
404 	 * #write(byte[], int, int)} are guaranteed to write at the end of the file,
405 	 * even if there have been intermediate seeks.
406 	 */
407 	private class PackStream extends OutputStream {
408 		final byte[] hdrBuf;
409 		final CRC32 crc32;
410 		final DeflaterOutputStream compress;
411 
412 		private final RandomAccessFile file;
413 		private final CountingOutputStream out;
414 		private final Deflater deflater;
415 
416 		private boolean atEnd;
417 
418 		PackStream(File pack) throws IOException {
419 			file = new RandomAccessFile(pack, "rw"); //$NON-NLS-1$
420 			out = new CountingOutputStream(new FileOutputStream(file.getFD()));
421 			deflater = new Deflater(compression);
422 			compress = new DeflaterOutputStream(this, deflater, 8192);
423 			hdrBuf = new byte[32];
424 			crc32 = new CRC32();
425 			atEnd = true;
426 		}
427 
428 		long getOffset() {
429 			// This value is accurate as long as we only ever write to the end of the
430 			// file, and don't seek back to overwrite any previous segments. Although
431 			// this is subtle, storing the stream counter this way is still preferable
432 			// to returning file.length() here, as it avoids a syscall and possible
433 			// IOException.
434 			return out.getCount();
435 		}
436 
437 		void seek(long offset) throws IOException {
438 			file.seek(offset);
439 			atEnd = false;
440 		}
441 
442 		void beginObject(int objectType, long length) throws IOException {
443 			crc32.reset();
444 			deflater.reset();
445 			write(hdrBuf, 0, encodeTypeSize(objectType, length));
446 		}
447 
448 		private int encodeTypeSize(int type, long rawLength) {
449 			long nextLength = rawLength >>> 4;
450 			hdrBuf[0] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (type << 4) | (rawLength & 0x0F));
451 			rawLength = nextLength;
452 			int n = 1;
453 			while (rawLength > 0) {
454 				nextLength >>>= 7;
455 				hdrBuf[n++] = (byte) ((nextLength > 0 ? 0x80 : 0x00) | (rawLength & 0x7F));
456 				rawLength = nextLength;
457 			}
458 			return n;
459 		}
460 
461 		@Override
462 		public void write(int b) throws IOException {
463 			hdrBuf[0] = (byte) b;
464 			write(hdrBuf, 0, 1);
465 		}
466 
467 		@Override
468 		public void write(byte[] data, int off, int len) throws IOException {
469 			crc32.update(data, off, len);
470 			if (!atEnd) {
471 				file.seek(file.length());
472 				atEnd = true;
473 			}
474 			out.write(data, off, len);
475 		}
476 
477 		byte[] finishPack() throws IOException {
478 			// Overwrite placeholder header with actual object count, then hash. This
479 			// method intentionally uses direct seek/write calls rather than the
480 			// wrappers which keep track of atEnd. This leaves atEnd, the file
481 			// pointer, and out's counter in an inconsistent state; that's ok, since
482 			// this method closes the file anyway.
483 			try {
484 				file.seek(0);
485 				out.write(hdrBuf, 0, writePackHeader(hdrBuf, objectList.size()));
486 
487 				byte[] buf = buffer();
488 				SHA1 md = digest().reset();
489 				file.seek(0);
490 				while (true) {
491 					int r = file.read(buf);
492 					if (r < 0) {
493 						break;
494 					}
495 					md.update(buf, 0, r);
496 				}
497 				byte[] packHash = md.digest();
498 				out.write(packHash, 0, packHash.length);
499 				return packHash;
500 			} finally {
501 				close();
502 			}
503 		}
504 
505 		@Override
506 		public void close() throws IOException {
507 			deflater.end();
508 			try {
509 				out.close();
510 			} finally {
511 				file.close();
512 			}
513 		}
514 
515 		byte[] inflate(long filePos, int len) throws IOException, DataFormatException {
516 			byte[] dstbuf;
517 			try {
518 				dstbuf = new byte[len];
519 			} catch (OutOfMemoryError noMemory) {
520 				return null; // Caller will switch to large object streaming.
521 			}
522 
523 			byte[] srcbuf = buffer();
524 			Inflater inf = inflater();
525 			filePos += setInput(filePos, inf, srcbuf);
526 			for (int dstoff = 0;;) {
527 				int n = inf.inflate(dstbuf, dstoff, dstbuf.length - dstoff);
528 				dstoff += n;
529 				if (inf.finished()) {
530 					return dstbuf;
531 				}
532 				if (inf.needsInput()) {
533 					filePos += setInput(filePos, inf, srcbuf);
534 				} else if (n == 0) {
535 					throw new DataFormatException();
536 				}
537 			}
538 		}
539 
540 		private int setInput(long filePos, Inflater inf, byte[] buf)
541 				throws IOException {
542 			if (file.getFilePointer() != filePos) {
543 				seek(filePos);
544 			}
545 			int n = file.read(buf);
546 			if (n < 0) {
547 				throw new EOFException(JGitText.get().unexpectedEofInPack);
548 			}
549 			inf.setInput(buf, 0, n);
550 			return n;
551 		}
552 	}
553 
554 	private class Reader extends ObjectReader {
555 		private final ObjectReader ctx;
556 
557 		private Reader() {
558 			ctx = db.newReader();
559 			setStreamFileThreshold(ctx.getStreamFileThreshold());
560 		}
561 
562 		@Override
563 		public ObjectReader newReader() {
564 			return db.newReader();
565 		}
566 
567 		@Override
568 		public ObjectInserter getCreatedFromInserter() {
569 			return PackInserter.this;
570 		}
571 
572 		@Override
573 		public Collection<ObjectId> resolve(AbbreviatedObjectId id)
574 				throws IOException {
575 			Collection<ObjectId> stored = ctx.resolve(id);
576 			if (objectList == null) {
577 				return stored;
578 			}
579 
580 			Set<ObjectId> r = new HashSet<>(stored.size() + 2);
581 			r.addAll(stored);
582 			for (PackedObjectInfo obj : objectList) {
583 				if (id.prefixCompare(obj) == 0) {
584 					r.add(obj.copy());
585 				}
586 			}
587 			return r;
588 		}
589 
590 		@Override
591 		public ObjectLoader open(AnyObjectId objectId, int typeHint)
592 				throws MissingObjectException, IncorrectObjectTypeException,
593 				IOException {
594 			if (objectMap == null) {
595 				return ctx.open(objectId, typeHint);
596 			}
597 
598 			PackedObjectInfo obj = objectMap.get(objectId);
599 			if (obj == null) {
600 				return ctx.open(objectId, typeHint);
601 			}
602 
603 			byte[] buf = buffer();
604 			packOut.seek(obj.getOffset());
605 			int cnt = packOut.file.read(buf, 0, 20);
606 			if (cnt <= 0) {
607 				throw new EOFException(JGitText.get().unexpectedEofInPack);
608 			}
609 
610 			int c = buf[0] & 0xff;
611 			int type = (c >> 4) & 7;
612 			if (type == OBJ_OFS_DELTA || type == OBJ_REF_DELTA) {
613 				throw new IOException(MessageFormat.format(
614 						JGitText.get().cannotReadBackDelta, Integer.toString(type)));
615 			}
616 			if (typeHint != OBJ_ANY && type != typeHint) {
617 				throw new IncorrectObjectTypeException(objectId.copy(), typeHint);
618 			}
619 
620 			long sz = c & 0x0f;
621 			int ptr = 1;
622 			int shift = 4;
623 			while ((c & 0x80) != 0) {
624 				if (ptr >= cnt) {
625 					throw new EOFException(JGitText.get().unexpectedEofInPack);
626 				}
627 				c = buf[ptr++] & 0xff;
628 				sz += ((long) (c & 0x7f)) << shift;
629 				shift += 7;
630 			}
631 
632 			long zpos = obj.getOffset() + ptr;
633 			if (sz < getStreamFileThreshold()) {
634 				byte[] data = inflate(obj, zpos, (int) sz);
635 				if (data != null) {
636 					return new ObjectLoader.SmallObject(type, data);
637 				}
638 			}
639 			return new StreamLoader(type, sz, zpos);
640 		}
641 
642 		private byte[] inflate(PackedObjectInfo obj, long zpos, int sz)
643 				throws IOException, CorruptObjectException {
644 			try {
645 				return packOut.inflate(zpos, sz);
646 			} catch (DataFormatException dfe) {
647 				throw new CorruptObjectException(
648 						MessageFormat.format(
649 								JGitText.get().objectAtHasBadZlibStream,
650 								Long.valueOf(obj.getOffset()),
651 								tmpPack.getAbsolutePath()),
652 						dfe);
653 			}
654 		}
655 
656 		@Override
657 		public Set<ObjectId> getShallowCommits() throws IOException {
658 			return ctx.getShallowCommits();
659 		}
660 
661 		@Override
662 		public void close() {
663 			ctx.close();
664 		}
665 
666 		private class StreamLoader extends ObjectLoader {
667 			private final int type;
668 			private final long size;
669 			private final long pos;
670 
671 			StreamLoader(int type, long size, long pos) {
672 				this.type = type;
673 				this.size = size;
674 				this.pos = pos;
675 			}
676 
677 			@Override
678 			public ObjectStream openStream()
679 					throws MissingObjectException, IOException {
680 				int bufsz = buffer().length;
681 				packOut.seek(pos);
682 
683 				InputStream fileStream = new FilterInputStream(
684 						Channels.newInputStream(packOut.file.getChannel())) {
685 							// atEnd was already set to false by the previous seek, but it's
686 							// technically possible for a caller to call insert on the
687 							// inserter in the middle of reading from this stream. Behavior is
688 							// undefined in this case, so it would arguably be ok to ignore,
689 							// but it's not hard to at least make an attempt to not corrupt
690 							// the data.
691 							@Override
692 							public int read() throws IOException {
693 								packOut.atEnd = false;
694 								return super.read();
695 							}
696 
697 							@Override
698 							public int read(byte[] b) throws IOException {
699 								packOut.atEnd = false;
700 								return super.read(b);
701 							}
702 
703 							@Override
704 							public int read(byte[] b, int off, int len) throws IOException {
705 								packOut.atEnd = false;
706 								return super.read(b,off,len);
707 							}
708 
709 							@Override
710 							public void close() {
711 								// Never close underlying RandomAccessFile, which lasts the
712 								// lifetime of the enclosing PackStream.
713 							}
714 						};
715 				return new ObjectStream.Filter(
716 						type, size,
717 						new BufferedInputStream(
718 								new InflaterInputStream(fileStream, inflater(), bufsz), bufsz));
719 			}
720 
721 			@Override
722 			public int getType() {
723 				return type;
724 			}
725 
726 			@Override
727 			public long getSize() {
728 				return size;
729 			}
730 
731 			@Override
732 			public byte[] getCachedBytes() throws LargeObjectException {
733 				throw new LargeObjectException.ExceedsLimit(
734 						getStreamFileThreshold(), size);
735 			}
736 		}
737 	}
738 }