/* * Copyright (C) 2017, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is * available at http://www.eclipse.org/org/documents/edl-v10.php * * All rights reserved. * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * - Neither the name of the Eclipse Foundation, Inc. nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.eclipse.jgit.internal.storage.file; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Random; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ObjectStream; import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.util.IO; import org.junit.After; import org.junit.Before; import org.junit.Test; @SuppressWarnings("boxing") public class PackInserterTest extends RepositoryTestCase { private WindowCacheConfig origWindowCacheConfig; private static final Random random = new Random(0); @Before public void setWindowCacheConfig() { origWindowCacheConfig = new WindowCacheConfig(); origWindowCacheConfig.install(); } @After public void resetWindowCacheConfig() { origWindowCacheConfig.install(); } @Before public void emptyAtSetUp() throws Exception { assertEquals(0, listPacks().size()); assertNoObjects(); } @Test public void noFlush() throws Exception { try (PackInserter ins = newInserter()) { ins.insert(OBJ_BLOB, Constants.encode("foo contents")); // No flush. } assertNoObjects(); } @Test public void flushEmptyPack() throws Exception { try (PackInserter ins = newInserter()) { ins.flush(); } assertNoObjects(); } @Test public void singlePack() throws Exception { ObjectId blobId; byte[] blob = Constants.encode("foo contents"); ObjectId treeId; ObjectId commitId; byte[] commit; try (PackInserter ins = newInserter()) { blobId = ins.insert(OBJ_BLOB, blob); DirCache dc = DirCache.newInCore(); DirCacheBuilder b = dc.builder(); DirCacheEntry dce = new DirCacheEntry("foo"); dce.setFileMode(FileMode.REGULAR_FILE); dce.setObjectId(blobId); b.add(dce); b.finish(); treeId = dc.writeTree(ins); CommitBuilder cb = new CommitBuilder(); cb.setTreeId(treeId); cb.setAuthor(author); cb.setCommitter(committer); cb.setMessage("Commit message"); commit = cb.toByteArray(); commitId = ins.insert(cb); ins.flush(); } assertPacksOnly(); List packs = listPacks(); assertEquals(1, packs.size()); assertEquals(3, packs.get(0).getObjectCount()); try (ObjectReader reader = db.newObjectReader()) { assertBlob(reader, blobId, blob); CanonicalTreeParser treeParser = new CanonicalTreeParser(null, reader, treeId); assertEquals("foo", treeParser.getEntryPathString()); assertEquals(blobId, treeParser.getEntryObjectId()); ObjectLoader commitLoader = reader.open(commitId); assertEquals(OBJ_COMMIT, commitLoader.getType()); assertArrayEquals(commit, commitLoader.getBytes()); } } @Test public void multiplePacks() throws Exception { ObjectId blobId1; ObjectId blobId2; byte[] blob1 = Constants.encode("blob1"); byte[] blob2 = Constants.encode("blob2"); try (PackInserter ins = newInserter()) { blobId1 = ins.insert(OBJ_BLOB, blob1); ins.flush(); blobId2 = ins.insert(OBJ_BLOB, blob2); ins.flush(); } assertPacksOnly(); List packs = listPacks(); assertEquals(2, packs.size()); assertEquals(1, packs.get(0).getObjectCount()); assertEquals(1, packs.get(1).getObjectCount()); try (ObjectReader reader = db.newObjectReader()) { assertBlob(reader, blobId1, blob1); assertBlob(reader, blobId2, blob2); } } @Test public void largeBlob() throws Exception { ObjectId blobId; byte[] blob = newLargeBlob(); try (PackInserter ins = newInserter()) { assertThat(blob.length, greaterThan(ins.getBufferSize())); blobId = ins.insert(OBJ_BLOB, blob.length, new ByteArrayInputStream(blob)); ins.flush(); } assertPacksOnly(); Collection packs = listPacks(); assertEquals(1, packs.size()); Pack p = packs.iterator().next(); assertEquals(1, p.getObjectCount()); try (ObjectReader reader = db.newObjectReader()) { assertBlob(reader, blobId, blob); } } @Test public void overwriteExistingPack() throws Exception { ObjectId blobId; byte[] blob = Constants.encode("foo contents"); try (PackInserter ins = newInserter()) { blobId = ins.insert(OBJ_BLOB, blob); ins.flush(); } assertPacksOnly(); List packs = listPacks(); assertEquals(1, packs.size()); Pack pack = packs.get(0); assertEquals(1, pack.getObjectCount()); String inode = getInode(pack.getPackFile()); try (PackInserter ins = newInserter()) { ins.checkExisting(false); assertEquals(blobId, ins.insert(OBJ_BLOB, blob)); ins.flush(); } assertPacksOnly(); packs = listPacks(); assertEquals(1, packs.size()); pack = packs.get(0); assertEquals(1, pack.getObjectCount()); if (inode != null) { // Old file was overwritten with new file, although objects were // equivalent. assertNotEquals(inode, getInode(pack.getPackFile())); } } @Test public void checkExisting() throws Exception { ObjectId blobId; byte[] blob = Constants.encode("foo contents"); try (PackInserter ins = newInserter()) { blobId = ins.insert(OBJ_BLOB, blob); ins.insert(OBJ_BLOB, Constants.encode("another blob")); ins.flush(); } assertPacksOnly(); assertEquals(1, listPacks().size()); try (PackInserter ins = newInserter()) { assertEquals(blobId, ins.insert(OBJ_BLOB, blob)); ins.flush(); } assertPacksOnly(); assertEquals(1, listPacks().size()); try (PackInserter ins = newInserter()) { ins.checkExisting(false); assertEquals(blobId, ins.insert(OBJ_BLOB, blob)); ins.flush(); } assertPacksOnly(); assertEquals(2, listPacks().size()); try (ObjectReader reader = db.newObjectReader()) { assertBlob(reader, blobId, blob); } } @Test public void insertSmallInputStreamRespectsCheckExisting() throws Exception { ObjectId blobId; byte[] blob = Constants.encode("foo contents"); try (PackInserter ins = newInserter()) { assertThat(blob.length, lessThan(ins.getBufferSize())); blobId = ins.insert(OBJ_BLOB, blob); ins.insert(OBJ_BLOB, Constants.encode("another blob")); ins.flush(); } assertPacksOnly(); assertEquals(1, listPacks().size()); try (PackInserter ins = newInserter()) { assertEquals(blobId, ins.insert(OBJ_BLOB, blob.length, new ByteArrayInputStream(blob))); ins.flush(); } assertPacksOnly(); assertEquals(1, listPacks().size()); } @Test public void insertLargeInputStreamBypassesCheckExisting() throws Exception { ObjectId blobId; byte[] blob = newLargeBlob(); try (PackInserter ins = newInserter()) { assertThat(blob.length, greaterThan(ins.getBufferSize())); blobId = ins.insert(OBJ_BLOB, blob); ins.insert(OBJ_BLOB, Constants.encode("another blob")); ins.flush(); } assertPacksOnly(); assertEquals(1, listPacks().size()); try (PackInserter ins = newInserter()) { assertEquals(blobId, ins.insert(OBJ_BLOB, blob.length, new ByteArrayInputStream(blob))); ins.flush(); } assertPacksOnly(); assertEquals(2, listPacks().size()); } @Test public void readBackSmallFiles() throws Exception { ObjectId blobId1; ObjectId blobId2; ObjectId blobId3; byte[] blob1 = Constants.encode("blob1"); byte[] blob2 = Constants.encode("blob2"); byte[] blob3 = Constants.encode("blob3"); try (PackInserter ins = newInserter()) { assertThat(blob1.length, lessThan(ins.getBufferSize())); blobId1 = ins.insert(OBJ_BLOB, blob1); try (ObjectReader reader = ins.newReader()) { assertBlob(reader, blobId1, blob1); } // Read-back should not mess up the file pointer. blobId2 = ins.insert(OBJ_BLOB, blob2); ins.flush(); blobId3 = ins.insert(OBJ_BLOB, blob3); } assertPacksOnly(); List packs = listPacks(); assertEquals(1, packs.size()); assertEquals(2, packs.get(0).getObjectCount()); try (ObjectReader reader = db.newObjectReader()) { assertBlob(reader, blobId1, blob1); assertBlob(reader, blobId2, blob2); try { reader.open(blobId3); fail("Expected MissingObjectException"); } catch (MissingObjectException expected) { // Expected. } } } @Test public void readBackLargeFile() throws Exception { ObjectId blobId; byte[] blob = newLargeBlob(); WindowCacheConfig wcc = new WindowCacheConfig(); wcc.setStreamFileThreshold(1024); wcc.install(); try (ObjectReader reader = db.newObjectReader()) { assertThat(blob.length, greaterThan(reader.getStreamFileThreshold())); } try (PackInserter ins = newInserter()) { blobId = ins.insert(OBJ_BLOB, blob); try (ObjectReader reader = ins.newReader()) { // Double-check threshold is propagated. assertThat(blob.length, greaterThan(reader.getStreamFileThreshold())); assertBlob(reader, blobId, blob); } } assertPacksOnly(); // Pack was streamed out to disk and read back from the temp file, but // ultimately rolled back and deleted. assertEquals(0, listPacks().size()); try (ObjectReader reader = db.newObjectReader()) { try { reader.open(blobId); fail("Expected MissingObjectException"); } catch (MissingObjectException expected) { // Expected. } } } @Test public void readBackFallsBackToRepo() throws Exception { ObjectId blobId; byte[] blob = Constants.encode("foo contents"); try (PackInserter ins = newInserter()) { assertThat(blob.length, lessThan(ins.getBufferSize())); blobId = ins.insert(OBJ_BLOB, blob); ins.flush(); } try (PackInserter ins = newInserter(); ObjectReader reader = ins.newReader()) { assertBlob(reader, blobId, blob); } } @Test public void readBackSmallObjectBeforeLargeObject() throws Exception { WindowCacheConfig wcc = new WindowCacheConfig(); wcc.setStreamFileThreshold(1024); wcc.install(); ObjectId blobId1; ObjectId blobId2; ObjectId largeId; byte[] blob1 = Constants.encode("blob1"); byte[] blob2 = Constants.encode("blob2"); byte[] largeBlob = newLargeBlob(); try (PackInserter ins = newInserter()) { assertThat(blob1.length, lessThan(ins.getBufferSize())); assertThat(largeBlob.length, greaterThan(ins.getBufferSize())); blobId1 = ins.insert(OBJ_BLOB, blob1); largeId = ins.insert(OBJ_BLOB, largeBlob); try (ObjectReader reader = ins.newReader()) { // A previous bug did not reset the file pointer to EOF after reading // back. We need to seek to something further back than a full buffer, // since the read-back code eagerly reads a full buffer's worth of data // from the file to pass to the inflater. If we seeked back just a small // amount, this step would consume the rest of the file, so the file // pointer would coincidentally end up back at EOF, hiding the bug. assertBlob(reader, blobId1, blob1); } blobId2 = ins.insert(OBJ_BLOB, blob2); try (ObjectReader reader = ins.newReader()) { assertBlob(reader, blobId1, blob1); assertBlob(reader, blobId2, blob2); assertBlob(reader, largeId, largeBlob); } ins.flush(); } try (ObjectReader reader = db.newObjectReader()) { assertBlob(reader, blobId1, blob1); assertBlob(reader, blobId2, blob2); assertBlob(reader, largeId, largeBlob); } } private List listPacks() throws Exception { List fromOpenDb = listPacks(db); List reopened; try (FileRepository db2 = new FileRepository(db.getDirectory())) { reopened = listPacks(db2); } assertEquals(fromOpenDb.size(), reopened.size()); for (int i = 0 ; i < fromOpenDb.size(); i++) { Pack a = fromOpenDb.get(i); Pack b = reopened.get(i); assertEquals(a.getPackName(), b.getPackName()); assertEquals( a.getPackFile().getAbsolutePath(), b.getPackFile().getAbsolutePath()); assertEquals(a.getObjectCount(), b.getObjectCount()); a.getObjectCount(); } return fromOpenDb; } private static List listPacks(FileRepository db) throws Exception { return db.getObjectDatabase().getPacks().stream() .sorted(comparing(Pack::getPackName)).collect(toList()); } private PackInserter newInserter() { return db.getObjectDatabase().newPackInserter(); } private static byte[] newLargeBlob() { byte[] blob = new byte[10240]; random.nextBytes(blob); return blob; } private static String getInode(File f) throws Exception { BasicFileAttributes attrs = Files.readAttributes( f.toPath(), BasicFileAttributes.class); Object k = attrs.fileKey(); if (k == null) { return null; } Pattern p = Pattern.compile("^\\(dev=[^,]*,ino=(\\d+)\\)$"); Matcher m = p.matcher(k.toString()); return m.matches() ? m.group(1) : null; } private static void assertBlob(ObjectReader reader, ObjectId id, byte[] expected) throws Exception { ObjectLoader loader = reader.open(id); assertEquals(OBJ_BLOB, loader.getType()); assertEquals(expected.length, loader.getSize()); try (ObjectStream s = loader.openStream()) { int n = (int) s.getSize(); byte[] actual = new byte[n]; assertEquals(n, IO.readFully(s, actual, 0)); assertArrayEquals(expected, actual); } } private void assertPacksOnly() throws Exception { new BadFileCollector(f -> !f.endsWith(".pack") && !f.endsWith(".idx")) .assertNoBadFiles(db.getObjectDatabase().getDirectory()); } private void assertNoObjects() throws Exception { new BadFileCollector(f -> true) .assertNoBadFiles(db.getObjectDatabase().getDirectory()); } private static class BadFileCollector extends SimpleFileVisitor { private final Predicate badName; private List bad; BadFileCollector(Predicate badName) { this.badName = badName; } void assertNoBadFiles(File f) throws IOException { bad = new ArrayList<>(); Files.walkFileTree(f.toPath(), this); if (!bad.isEmpty()) { fail("unexpected files in object directory: " + bad); } } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { Path fileName = file.getFileName(); if (fileName != null) { String name = fileName.toString(); if (!attrs.isDirectory() && badName.test(name)) { bad.add(name); } } return FileVisitResult.CONTINUE; } } }