diff options
19 files changed, 885 insertions, 197 deletions
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java index 41217d9971..c88670ec97 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java @@ -76,6 +76,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.UnpackException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.InternalHttpServerGlue; @@ -200,7 +201,7 @@ class ReceivePackServlet extends HttpServlet { consumeRequestBody(req); out.close(); - } catch (UnpackException e) { + } catch (UnpackException | PackProtocolException e) { // This should be already reported to the client. log(rp.getRepository(), e.getCause()); consumeRequestBody(req); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java index 0742504d23..bbd41237e2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java @@ -45,13 +45,16 @@ package org.eclipse.jgit.internal.storage.file; import static org.junit.Assert.assertEquals; +import java.io.File; import java.io.IOException; import java.util.Collection; +import java.util.Date; import java.util.Iterator; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.pack.PackConfig; +import org.junit.Test; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.Theories; import org.junit.experimental.theories.Theory; @@ -176,6 +179,47 @@ public class GcBasicPackingTest extends GcTestCase { } } + @Test + public void testDonePruneTooYoungPacks() throws Exception { + BranchBuilder bb = tr.branch("refs/heads/master"); + bb.commit().message("M").add("M", "M").create(); + + gc.setExpireAgeMillis(0); + gc.gc(); + stats = gc.getStatistics(); + assertEquals(0, stats.numberOfLooseObjects); + assertEquals(3, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + File oldPackfile = tr.getRepository().getObjectDatabase().getPacks() + .iterator().next().getPackFile(); + + fsTick(); + bb.commit().message("B").add("B", "Q").create(); + + // The old packfile is too young to be deleted. We should end up with + // two pack files + gc.setExpire(new Date(oldPackfile.lastModified() - 1)); + gc.gc(); + stats = gc.getStatistics(); + assertEquals(0, stats.numberOfLooseObjects); + // if objects exist in multiple packFiles then they are counted multiple + // times + assertEquals(9, stats.numberOfPackedObjects); + assertEquals(2, stats.numberOfPackFiles); + + // repack again but now without a grace period for packfiles. We should + // end up with one packfile + gc.setExpireAgeMillis(0); + gc.gc(); + stats = gc.getStatistics(); + assertEquals(0, stats.numberOfLooseObjects); + // if objects exist in multiple packFiles then they are counted multiple + // times + assertEquals(6, stats.numberOfPackedObjects); + assertEquals(1, stats.numberOfPackFiles); + + } + private void configureGc(GC myGc, boolean aggressive) { PackConfig pconfig = new PackConfig(repo); if (aggressive) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java index abf57d6cd3..2198b87aa5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java @@ -49,6 +49,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import org.eclipse.jgit.errors.InvalidObjectIdException; + import org.junit.Test; public class ObjectIdTest { @@ -124,6 +126,21 @@ public class ObjectIdTest { assertEquals(x.toLowerCase(), oid.name()); } + @Test(expected = InvalidObjectIdException.class) + public void testFromString_short() { + ObjectId.fromString("cafe1234"); + } + + @Test(expected = InvalidObjectIdException.class) + public void testFromString_nonHex() { + ObjectId.fromString("0123456789abcdefghij0123456789abcdefghij"); + } + + @Test(expected = InvalidObjectIdException.class) + public void testFromString_shortNonHex() { + ObjectId.fromString("6789ghij"); + } + @Test public void testGetByte() { byte[] raw = new byte[20]; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java index 0cab987e6a..df9e0294e8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java @@ -43,11 +43,14 @@ package org.eclipse.jgit.lib; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -59,6 +62,7 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.junit.Test; +@SuppressWarnings("boxing") public class RepositoryCacheTest extends RepositoryTestCase { @Test public void testNonBareFileKey() throws IOException { @@ -147,4 +151,28 @@ public class RepositoryCacheTest extends RepositoryTestCase { d2.close(); d2.close(); } + + @Test + public void testGetRegisteredWhenEmpty() { + assertThat(RepositoryCache.getRegisteredKeys().size(), is(0)); + } + + @Test + public void testGetRegistered() { + RepositoryCache.register(db); + + assertThat(RepositoryCache.getRegisteredKeys(), + hasItem(FileKey.exact(db.getDirectory(), db.getFS()))); + assertThat(RepositoryCache.getRegisteredKeys().size(), is(1)); + } + + @Test + public void testUnregister() { + RepositoryCache.register(db); + RepositoryCache + .unregister(FileKey.exact(db.getDirectory(), db.getFS())); + + assertThat(RepositoryCache.getRegisteredKeys().size(), is(0)); + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BaseReceivePackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BaseReceivePackTest.java index 98164d9335..7578c6e3eb 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BaseReceivePackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BaseReceivePackTest.java @@ -13,17 +13,17 @@ * conditions are met: * * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. + * 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. + * 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. + * 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, @@ -43,28 +43,43 @@ package org.eclipse.jgit.transport; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; /** Tests for base receive-pack utilities. */ public class BaseReceivePackTest { @Test - public void chomp() { - assertEquals("foo", BaseReceivePack.chomp("foo")); - assertEquals("foo", BaseReceivePack.chomp("foo\n")); - assertEquals("foo\n", BaseReceivePack.chomp("foo\n\n")); - } - - @Test - public void parseCommand() { - String input = "0000000000000000000000000000000000000000" - + " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" - + " refs/heads/master"; - ReceiveCommand cmd = BaseReceivePack.parseCommand(input); + public void parseCommand() throws Exception { + String o = "0000000000000000000000000000000000000000"; + String n = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + String r = "refs/heads/master"; + ReceiveCommand cmd = BaseReceivePack.parseCommand(o + " " + n + " " + r); assertEquals(ObjectId.zeroId(), cmd.getOldId()); assertEquals("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", cmd.getNewId().name()); assertEquals("refs/heads/master", cmd.getRefName()); + + assertParseCommandFails(null); + assertParseCommandFails(""); + assertParseCommandFails(o.substring(35) + " " + n.substring(35) + + " " + r + "\n"); + assertParseCommandFails(o + " " + n + " " + r + "\n"); + assertParseCommandFails(o + " " + n + " " + "refs^foo"); + assertParseCommandFails(o + " " + n.substring(10) + " " + r); + assertParseCommandFails(o.substring(10) + " " + n + " " + r); + assertParseCommandFails("X" + o.substring(1) + " " + n + " " + r); + assertParseCommandFails(o + " " + "X" + n.substring(1) + " " + r); + } + + private void assertParseCommandFails(String input) { + try { + BaseReceivePack.parseCommand(input); + fail(); + } catch (PackProtocolException e) { + // Expected. + } } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java index 9c157c3379..3a4b00d876 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java @@ -45,6 +45,7 @@ package org.eclipse.jgit.transport; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -52,6 +53,8 @@ import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; @@ -60,6 +63,7 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.PushCertificate.NonceStatus; import org.junit.Before; import org.junit.Test; @@ -87,6 +91,30 @@ public class PushCertificateParserTest { + "0020-----END PGP SIGNATURE-----\n" + "0012push-cert-end\n"; + // Same push certificate, with all trailing newlines stripped. + // (Note that the canonical signed payload is the same, so the same signature + // is still valid.) + private static final String INPUT_NO_NEWLINES = "001bcertificate version 0.1" + + "0040pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700" + + "0023pushee git://localhost/repo.git" + + "0029nonce 1433954361-bde756572d665bba81d8" + + "0004" + + "00670000000000000000000000000000000000000000" + + " 6c2b981a177396fb47345b7df3e4d3f854c6bea7" + + " refs/heads/master" + + "0021-----BEGIN PGP SIGNATURE-----" + + "0015Version: GnuPG v1" + + "0004" + + "0044iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa" + + "00449tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7" + + "0044htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V" + + "00444ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG" + + "0044IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY" + + "0044+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=" + + "0009=XFeC" + + "001f-----END PGP SIGNATURE-----" + + "0011push-cert-end"; + private Repository db; @Before @@ -114,12 +142,11 @@ public class PushCertificateParserTest { ObjectId oldId = ObjectId.zeroId(); ObjectId newId = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); - String rawLine = - oldId.name() + " " + newId.name() + " refs/heads/master"; - ReceiveCommand cmd = BaseReceivePack.parseCommand(rawLine); + String line = oldId.name() + " " + newId.name() + " refs/heads/master"; + ReceiveCommand cmd = BaseReceivePack.parseCommand(line); - parser.addCommand(cmd, rawLine); - parser.addCommand(rawLine); + parser.addCommand(cmd); + parser.addCommand(line); assertNull(parser.build()); } @@ -132,8 +159,8 @@ public class PushCertificateParserTest { assertNull(parser.build()); parser.receiveHeader(pckIn, false); - parser.addCommand(pckIn.readStringRaw()); - assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readStringRaw()); + parser.addCommand(pckIn.readString()); + assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); parser.receiveSignature(pckIn); assertNull(parser.build()); } @@ -162,8 +189,8 @@ public class PushCertificateParserTest { PushCertificateParser parser = new PushCertificateParser(db, newEnabledConfig()); parser.receiveHeader(pckIn, false); - parser.addCommand(pckIn.readStringRaw()); - assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readStringRaw()); + parser.addCommand(pckIn.readString()); + assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); parser.receiveSignature(pckIn); PushCertificate cert = parser.build(); @@ -190,7 +217,46 @@ public class PushCertificateParserTest { String signature = concatPacketLines(INPUT, 6, 17); assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE)); - assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE)); + assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n")); + assertEquals(signature, cert.getSignature()); + } + + @Test + public void parseCertFromPktLineNoNewlines() throws Exception { + PacketLineIn pckIn = newPacketLineIn(INPUT_NO_NEWLINES); + PushCertificateParser parser = + new PushCertificateParser(db, newEnabledConfig()); + parser.receiveHeader(pckIn, false); + parser.addCommand(pckIn.readString()); + assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); + parser.receiveSignature(pckIn); + + PushCertificate cert = parser.build(); + assertEquals("0.1", cert.getVersion()); + assertEquals("Dave Borowitz", cert.getPusherIdent().getName()); + assertEquals("dborowitz@google.com", + cert.getPusherIdent().getEmailAddress()); + assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime()); + assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset()); + assertEquals("git://localhost/repo.git", cert.getPushee()); + assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce()); + + assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce()); + assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus()); + + assertEquals(1, cert.getCommands().size()); + ReceiveCommand cmd = cert.getCommands().get(0); + assertEquals("refs/heads/master", cmd.getRefName()); + assertEquals(ObjectId.zeroId(), cmd.getOldId()); + assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7", + cmd.getNewId().name()); + + // Canonical signed payload has reinserted newlines. + assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText()); + + String signature = concatPacketLines(INPUT, 6, 17); + assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE)); + assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n")); assertEquals(signature, cert.getSignature()); } @@ -203,6 +269,91 @@ public class PushCertificateParserTest { assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4)); } + @Test + public void testConcatPacketLinesInsertsNewlines() throws Exception { + String input = "000bline 1\n000aline 2000bline 3\n"; + assertEquals("line 1\n", concatPacketLines(input, 0, 1)); + assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2)); + assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3)); + assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4)); + } + + @Test + public void testParseReader() throws Exception { + Reader reader = new InputStreamReader( + new ByteArrayInputStream( + Constants.encode(concatPacketLines(INPUT, 0, 18)))); + PushCertificate streamCert = PushCertificateParser.fromReader(reader); + + PacketLineIn pckIn = newPacketLineIn(INPUT); + PushCertificateParser pckParser = + new PushCertificateParser(db, newEnabledConfig()); + pckParser.receiveHeader(pckIn, false); + pckParser.addCommand(pckIn.readString()); + assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); + pckParser.receiveSignature(pckIn); + PushCertificate pckCert = pckParser.build(); + + // Nonce status is unsolicited since this was not parsed in the context of + // the wire protocol; as a result, certs are not actually equal. + assertEquals(NonceStatus.UNSOLICITED, streamCert.getNonceStatus()); + + assertEquals(pckCert.getVersion(), streamCert.getVersion()); + assertEquals(pckCert.getPusherIdent().getName(), + streamCert.getPusherIdent().getName()); + assertEquals(pckCert.getPusherIdent().getEmailAddress(), + streamCert.getPusherIdent().getEmailAddress()); + assertEquals(pckCert.getPusherIdent().getWhen().getTime(), + streamCert.getPusherIdent().getWhen().getTime()); + assertEquals(pckCert.getPusherIdent().getTimeZoneOffset(), + streamCert.getPusherIdent().getTimeZoneOffset()); + assertEquals(pckCert.getPushee(), streamCert.getPushee()); + assertEquals(pckCert.getNonce(), streamCert.getNonce()); + assertEquals(pckCert.getSignature(), streamCert.getSignature()); + assertEquals(pckCert.toText(), streamCert.toText()); + + assertEquals(pckCert.getCommands().size(), streamCert.getCommands().size()); + ReceiveCommand pckCmd = pckCert.getCommands().get(0); + ReceiveCommand streamCmd = streamCert.getCommands().get(0); + assertEquals(pckCmd.getRefName(), streamCmd.getRefName()); + assertEquals(pckCmd.getOldId(), streamCmd.getOldId()); + assertEquals(pckCmd.getNewId().name(), streamCmd.getNewId().name()); + } + + @Test + public void testParseMultipleFromStream() throws Exception { + String input = concatPacketLines(INPUT, 0, 17); + assertFalse(input.contains(PushCertificateParser.END_CERT)); + input += input; + Reader reader = new InputStreamReader( + new ByteArrayInputStream(Constants.encode(input))); + + assertNotNull(PushCertificateParser.fromReader(reader)); + assertNotNull(PushCertificateParser.fromReader(reader)); + assertEquals(-1, reader.read()); + assertNull(PushCertificateParser.fromReader(reader)); + } + + @Test + public void testMissingPusheeField() throws Exception { + // Omit pushee line from existing cert. (This means the signature would not + // match, but we're not verifying it here.) + String input = INPUT.replace("0024pushee git://localhost/repo.git\n", ""); + assertFalse(input.contains(PushCertificateParser.PUSHEE)); + + PacketLineIn pckIn = newPacketLineIn(input); + PushCertificateParser parser = + new PushCertificateParser(db, newEnabledConfig()); + parser.receiveHeader(pckIn, false); + parser.addCommand(pckIn.readString()); + assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString()); + parser.receiveSignature(pckIn); + + PushCertificate cert = parser.build(); + assertEquals("0.1", cert.getVersion()); + assertNull(cert.getPushee()); + } + private static String concatPacketLines(String input, int begin, int end) throws IOException { StringBuilder result = new StringBuilder(); @@ -211,12 +362,12 @@ public class PushCertificateParserTest { while (i < end) { String line; try { - line = pckIn.readStringRaw(); + line = pckIn.readString(); } catch (EOFException e) { break; } if (++i > begin) { - result.append(line); + result.append(line).append('\n'); } } return result.toString(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOReadLineTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOReadLineTest.java new file mode 100644 index 0000000000..928fb2ed9a --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOReadLineTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2015, 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.util; + +import static org.junit.Assert.assertEquals; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collection; + +import org.eclipse.jgit.lib.Constants; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class IOReadLineTest { + @Parameter(0) + public boolean buffered; + + @Parameter(1) + public int sizeHint; + + @SuppressWarnings("boxing") + @Parameters(name="buffered={0}, sizeHint={1}") + public static Collection<Object[]> getParameters() { + Boolean[] bv = {false, true}; + Integer[] sv = {-1, 0, 1, 2, 3, 4, 64}; + Collection<Object[]> params = new ArrayList<>(bv.length * sv.length); + for (boolean b : bv) { + for (Integer s : sv) { + params.add(new Object[]{b, s}); + } + } + return params; + } + + @Test + public void testReadLine() throws Exception { + Reader r = newReader("foo\nbar\nbaz\n"); + assertEquals("foo\n", readLine(r)); + assertEquals("bar\n", readLine(r)); + assertEquals("baz\n", readLine(r)); + assertEquals("", readLine(r)); + } + + @Test + public void testReadLineNoTrailingNewline() throws Exception { + Reader r = newReader("foo\nbar\nbaz"); + assertEquals("foo\n", readLine(r)); + assertEquals("bar\n", readLine(r)); + assertEquals("baz", readLine(r)); + assertEquals("", readLine(r)); + } + + private String readLine(Reader r) throws Exception { + return IO.readLine(r, sizeHint); + } + + private Reader newReader(String in) { + Reader r = new InputStreamReader( + new ByteArrayInputStream(Constants.encode(in))); + if (buffered) { + r = new BufferedReader(r); + } + assertEquals(Boolean.valueOf(buffered), + Boolean.valueOf(r.markSupported())); + return r; + } +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 509027dafc..bc8b8bfc05 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -59,23 +59,23 @@ cannotCreateIndexfile=Cannot create an index file with name {0} cannotCreateTempDir=Cannot create a temp dir cannotDeleteCheckedOutBranch=Branch {0} is checked out and can not be deleted cannotDeleteFile=Cannot delete file: {0} -cannotDeleteObjectsPath="Can't delete {0}/{1}: {2} +cannotDeleteObjectsPath=Cannot delete {0}/{1}: {2} cannotDeleteStaleTrackingRef=Cannot delete stale tracking ref {0} cannotDeleteStaleTrackingRef2=Cannot delete stale tracking ref {0}: {1} cannotDetermineProxyFor=Cannot determine proxy for {0} cannotDownload=Cannot download {0} -cannotEnterObjectsPath=Can't enter {0}/objects: {1} -cannotEnterPathFromParent=Can't enter {0} from {1}: {2} +cannotEnterObjectsPath=Cannot enter {0}/objects: {1} +cannotEnterPathFromParent=Cannot enter {0} from {1}: {2} cannotExecute=cannot execute: {0} cannotGet=Cannot get {0} -cannotGetObjectsPath=Can't get {0}/{1}: {2} -cannotListObjectsPath=Can't ls {0}/{1}: {2} -cannotListPackPath=Can't ls {0}/pack: {1} +cannotGetObjectsPath=Cannot get {0}/{1}: {2} +cannotListObjectsPath=Cannot ls {0}/{1}: {2} +cannotListPackPath=Cannot ls {0}/pack: {1} cannotListRefs=cannot list refs cannotLock=Cannot lock {0} cannotLockPackIn=Cannot lock pack in {0} cannotMatchOnEmptyString=Cannot match on empty string. -cannotMkdirObjectPath=Can't mkdir {0}/{1}: {2} +cannotMkdirObjectPath=Cannot mkdir {0}/{1}: {2} cannotMoveIndexTo=Cannot move index to {0} cannotMovePackTo=Cannot move pack to {0} cannotOpenService=cannot open {0} @@ -97,7 +97,7 @@ cannotStoreObjects=cannot store objects cannotResolveUniquelyAbbrevObjectId=Could not resolve uniquely the abbreviated object ID cannotUnloadAModifiedTree=Cannot unload a modified tree. cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index. -cannotWriteObjectsPath="Can't write {0}/{1}: {2} +cannotWriteObjectsPath=Cannot write {0}/{1}: {2} canOnlyCherryPickCommitsWithOneParent=Cannot cherry-pick commit ''{0}'' because it has {1} parents, only commits with exactly one parent are supported. canOnlyRevertCommitsWithOneParent=Cannot revert commit ''{0}'' because it has {1} parents, only commits with exactly one parent are supported commitDoesNotHaveGivenParent=The commit ''{0}'' does not have a parent number {1}. @@ -325,6 +325,7 @@ invalidEncryption=Invalid encryption invalidGitdirRef = Invalid .git reference in file ''{0}'' invalidGitType=invalid git type: {0} invalidId=Invalid id: {0} +invalidId0=Invalid id invalidIdLength=Invalid id length {0}; should be {1} invalidIgnoreParamSubmodule=Found invalid ignore param for submodule {0}. invalidIntegerValue=Invalid integer value: {0}.{1}={2} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java index b545312ae7..390545ffaf 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java @@ -45,7 +45,8 @@ package org.eclipse.jgit.errors; -import java.io.UnsupportedEncodingException; +import static java.nio.charset.StandardCharsets.US_ASCII; + import java.text.MessageFormat; import org.eclipse.jgit.internal.JGitText; @@ -64,16 +65,25 @@ public class InvalidObjectIdException extends IllegalArgumentException { * @param length of the sequence of invalid bytes. */ public InvalidObjectIdException(byte[] bytes, int offset, int length) { - super(MessageFormat.format(JGitText.get().invalidId, asAscii(bytes, offset, length))); + super(msg(bytes, offset, length)); + } + + /** + * @param id the invalid id. + * + * @since 4.1 + */ + public InvalidObjectIdException(String id) { + super(MessageFormat.format(JGitText.get().invalidId, id)); } - private static String asAscii(byte[] bytes, int offset, int length) { + private static String msg(byte[] bytes, int offset, int length) { try { - return ": " + new String(bytes, offset, length, "US-ASCII"); //$NON-NLS-1$ //$NON-NLS-2$ - } catch (UnsupportedEncodingException e2) { - return ""; //$NON-NLS-1$ - } catch (StringIndexOutOfBoundsException e2) { - return ""; //$NON-NLS-1$ + return MessageFormat.format( + JGitText.get().invalidId, + new String(bytes, offset, length, US_ASCII)); + } catch (StringIndexOutOfBoundsException e) { + return JGitText.get().invalidId0; } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java index 5503bd19e2..44bc16492d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java @@ -60,7 +60,7 @@ public class PackProtocolException extends TransportException { * @param uri * URI used for transport * @param s - * message + * message, which may be shown to an end-user. */ public PackProtocolException(final URIish uri, final String s) { super(uri + ": " + s); //$NON-NLS-1$ @@ -73,7 +73,7 @@ public class PackProtocolException extends TransportException { * @param uri * URI used for transport * @param s - * message + * message, which may be shown to an end-user. * @param cause * root cause exception */ @@ -86,7 +86,7 @@ public class PackProtocolException extends TransportException { * Constructs an PackProtocolException with the specified detail message. * * @param s - * message + * message, which may be shown to an end-user. */ public PackProtocolException(final String s) { super(s); @@ -96,7 +96,7 @@ public class PackProtocolException extends TransportException { * Constructs an PackProtocolException with the specified detail message. * * @param s - * message + * message, which may be shown to an end-user. * @param cause * root cause exception */ diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index f903f23377..86f277ac39 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -384,6 +384,7 @@ public class JGitText extends TranslationBundle { /***/ public String invalidGitdirRef; /***/ public String invalidGitType; /***/ public String invalidId; + /***/ public String invalidId0; /***/ public String invalidIdLength; /***/ public String invalidIgnoreParamSubmodule; /***/ public String invalidIntegerValue; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java index 338106f8e7..c5723c0594 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java @@ -175,13 +175,17 @@ public class GC { /** * Delete old pack files. What is 'old' is defined by specifying a set of * old pack files and a set of new pack files. Each pack file contained in - * old pack files but not contained in new pack files will be deleted. + * old pack files but not contained in new pack files will be deleted. If an + * expirationDate is set then pack files which are younger than the + * expirationDate will not be deleted. * * @param oldPacks * @param newPacks + * @throws ParseException */ private void deleteOldPacks(Collection<PackFile> oldPacks, - Collection<PackFile> newPacks) { + Collection<PackFile> newPacks) throws ParseException { + long expireDate = getExpireDate(); oldPackLoop: for (PackFile oldPack : oldPacks) { String oldName = oldPack.getPackName(); // check whether an old pack file is also among the list of new @@ -190,7 +194,8 @@ public class GC { if (oldName.equals(newPack.getPackName())) continue oldPackLoop; - if (!oldPack.shouldBeKept()) { + if (!oldPack.shouldBeKept() + && oldPack.getPackFile().lastModified() < expireDate) { oldPack.close(); prunePack(oldName); } @@ -303,22 +308,7 @@ public class GC { */ public void prune(Set<ObjectId> objectsToKeep) throws IOException, ParseException { - long expireDate = Long.MAX_VALUE; - - if (expire == null && expireAgeMillis == -1) { - String pruneExpireStr = repo.getConfig().getString( - ConfigConstants.CONFIG_GC_SECTION, null, - ConfigConstants.CONFIG_KEY_PRUNEEXPIRE); - if (pruneExpireStr == null) - pruneExpireStr = PRUNE_EXPIRE_DEFAULT; - expire = GitDateParser.parse(pruneExpireStr, null, SystemReader - .getInstance().getLocale()); - expireAgeMillis = -1; - } - if (expire != null) - expireDate = expire.getTime(); - if (expireAgeMillis != -1) - expireDate = System.currentTimeMillis() - expireAgeMillis; + long expireDate = getExpireDate(); // Collect all loose objects which are old enough, not referenced from // the index and not in objectsToKeep @@ -435,6 +425,26 @@ public class GC { repo.getObjectDatabase().close(); } + private long getExpireDate() throws ParseException { + long expireDate = Long.MAX_VALUE; + + if (expire == null && expireAgeMillis == -1) { + String pruneExpireStr = repo.getConfig().getString( + ConfigConstants.CONFIG_GC_SECTION, null, + ConfigConstants.CONFIG_KEY_PRUNEEXPIRE); + if (pruneExpireStr == null) + pruneExpireStr = PRUNE_EXPIRE_DEFAULT; + expire = GitDateParser.parse(pruneExpireStr, null, SystemReader + .getInstance().getLocale()); + expireAgeMillis = -1; + } + if (expire != null) + expireDate = expire.getTime(); + if (expireAgeMillis != -1) + expireDate = System.currentTimeMillis() - expireAgeMillis; + return expireDate; + } + /** * Remove all entries from a map which key is the id of an object referenced * by the given ObjectWalk @@ -559,7 +569,14 @@ public class GC { if (rest != null) ret.add(rest); } - deleteOldPacks(toBeDeleted, ret); + try { + deleteOldPacks(toBeDeleted, ret); + } catch (ParseException e) { + // TODO: the exception has to be wrapped into an IOException because + // throwing the ParseException directly would break the API, instead + // we should throw a ConfigInvalidException + throw new IOException(e); + } prunePacked(); lastPackedRefs = refsBefore; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java index bdbffee490..de7e207ac4 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java @@ -45,7 +45,6 @@ package org.eclipse.jgit.lib; import org.eclipse.jgit.errors.InvalidObjectIdException; -import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.NB; import org.eclipse.jgit.util.RawParseUtils; @@ -53,7 +52,6 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; -import java.text.MessageFormat; /** * A SHA-1 abstraction. @@ -230,9 +228,9 @@ public class ObjectId extends AnyObjectId implements Serializable { * @return the converted object id. */ public static ObjectId fromString(final String str) { - if (str.length() != Constants.OBJECT_ID_STRING_LENGTH) - throw new IllegalArgumentException( - MessageFormat.format(JGitText.get().invalidId, str)); + if (str.length() != Constants.OBJECT_ID_STRING_LENGTH) { + throw new InvalidObjectIdException(str); + } return fromHexString(Constants.encodeASCII(str), 0); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java index 0c58a0bea4..23cc264c1c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java @@ -47,6 +47,8 @@ import java.io.File; import java.io.IOException; import java.lang.ref.Reference; import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -143,6 +145,28 @@ public class RepositoryCache { } } + /** + * Remove a repository from the cache. + * <p> + * Removes a repository from the cache, if it is still registered here, + * permitting it to close. + * + * @param location + * location of the repository to remove. + * @since 4.1 + */ + public static void unregister(Key location) { + cache.unregisterRepository(location); + } + + /** + * @return the locations of all repositories registered in the cache. + * @since 4.1 + */ + public static Collection<Key> getRegisteredKeys() { + return cache.getKeys(); + } + /** Unregister all repositories from the cache. */ public static void clear() { cache.clearAll(); @@ -195,6 +219,10 @@ public class RepositoryCache { oldDb.close(); } + private Collection<Key> getKeys() { + return new ArrayList<Key>(cacheMap.keySet()); + } + private void clearAll() { for (int stage = 0; stage < 2; stage++) { for (Iterator<Map.Entry<Key, Reference<Repository>>> i = cacheMap diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index 37e5d3cd3c..518a3178f6 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -1065,74 +1065,84 @@ public abstract class BaseReceivePack { protected void recvCommands() throws IOException { PushCertificateParser certParser = getPushCertificateParser(); FirstLine firstLine = null; - for (;;) { - String rawLine; - try { - rawLine = pckIn.readStringRaw(); - } catch (EOFException eof) { - if (commands.isEmpty()) - return; - throw eof; - } - if (rawLine == PacketLineIn.END) { - break; - } - String line = chomp(rawLine); - - if (line.length() >= 48 && line.startsWith("shallow ")) { //$NON-NLS-1$ - clientShallowCommits.add(ObjectId.fromString(line.substring(8, 48))); - continue; - } - - if (firstLine == null) { - firstLine = new FirstLine(line); - enabledCapabilities = firstLine.getCapabilities(); - line = firstLine.getLine(); + try { + for (;;) { + String line; + try { + line = pckIn.readString(); + } catch (EOFException eof) { + if (commands.isEmpty()) + return; + throw eof; + } + if (line == PacketLineIn.END) { + break; + } - if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) { - certParser.receiveHeader(pckIn, !isBiDirectionalPipe()); + if (line.length() >= 48 && line.startsWith("shallow ")) { //$NON-NLS-1$ + clientShallowCommits.add(ObjectId.fromString(line.substring(8, 48))); continue; } - } - if (rawLine.equals(PushCertificateParser.BEGIN_SIGNATURE)) { - certParser.receiveSignature(pckIn); - continue; - } + if (firstLine == null) { + firstLine = new FirstLine(line); + enabledCapabilities = firstLine.getCapabilities(); + line = firstLine.getLine(); - if (line.length() < 83) { - final String m = JGitText.get().errorInvalidProtocolWantedOldNewRef; - sendError(m); - throw new PackProtocolException(m); - } + if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) { + certParser.receiveHeader(pckIn, !isBiDirectionalPipe()); + continue; + } + } - final ReceiveCommand cmd = parseCommand(line); - if (cmd.getRefName().equals(Constants.HEAD)) { - cmd.setResult(Result.REJECTED_CURRENT_BRANCH); - } else { - cmd.setRef(refs.get(cmd.getRefName())); - } - commands.add(cmd); - if (certParser.enabled()) { - // Must use raw line with optional newline so signed payload can be - // reconstructed. - certParser.addCommand(cmd, rawLine); + if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) { + certParser.receiveSignature(pckIn); + continue; + } + + ReceiveCommand cmd; + try { + cmd = parseCommand(line); + } catch (PackProtocolException e) { + sendError(e.getMessage()); + throw e; + } + if (cmd.getRefName().equals(Constants.HEAD)) { + cmd.setResult(Result.REJECTED_CURRENT_BRANCH); + } else { + cmd.setRef(refs.get(cmd.getRefName())); + } + commands.add(cmd); + if (certParser.enabled()) { + certParser.addCommand(cmd); + } } + } catch (PackProtocolException e) { + sendError(e.getMessage()); + throw e; } } - static String chomp(String line) { - if (line != null && !line.isEmpty() - && line.charAt(line.length() - 1) == '\n') { - return line.substring(0, line.length() - 1); + static ReceiveCommand parseCommand(String line) throws PackProtocolException { + if (line == null || line.length() < 83) { + throw new PackProtocolException( + JGitText.get().errorInvalidProtocolWantedOldNewRef); + } + String oldStr = line.substring(0, 40); + String newStr = line.substring(41, 81); + ObjectId oldId, newId; + try { + oldId = ObjectId.fromString(oldStr); + newId = ObjectId.fromString(newStr); + } catch (IllegalArgumentException e) { + throw new PackProtocolException( + JGitText.get().errorInvalidProtocolWantedOldNewRef, e); } - return line; - } - - static ReceiveCommand parseCommand(String line) { - ObjectId oldId = ObjectId.fromString(line.substring(0, 40)); - ObjectId newId = ObjectId.fromString(line.substring(41, 81)); String name = line.substring(82); + if (!Repository.isValidRefName(name)) { + throw new PackProtocolException( + JGitText.get().errorInvalidProtocolWantedOldNewRef); + } return new ReceiveCommand(oldId, newId, name); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java index 6dc4153d1e..413b02b1c8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java @@ -50,6 +50,7 @@ import static org.eclipse.jgit.transport.PushCertificateParser.VERSION; import java.text.MessageFormat; import java.util.List; +import java.util.Objects; import org.eclipse.jgit.internal.JGitText; @@ -85,12 +86,11 @@ public class PushCertificate { private final String nonce; private final NonceStatus nonceStatus; private final List<ReceiveCommand> commands; - private final String rawCommands; private final String signature; PushCertificate(String version, PushCertificateIdent pusher, String pushee, String nonce, NonceStatus nonceStatus, List<ReceiveCommand> commands, - String rawCommands, String signature) { + String signature) { if (version == null || version.isEmpty()) { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().pushCertificateInvalidField, VERSION)); @@ -99,10 +99,6 @@ public class PushCertificate { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().pushCertificateInvalidField, PUSHER)); } - if (pushee == null || pushee.isEmpty()) { - throw new IllegalArgumentException(MessageFormat.format( - JGitText.get().pushCertificateInvalidField, PUSHEE)); - } if (nonce == null || nonce.isEmpty()) { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().pushCertificateInvalidField, NONCE)); @@ -112,8 +108,7 @@ public class PushCertificate { JGitText.get().pushCertificateInvalidField, "nonce status")); //$NON-NLS-1$ } - if (commands == null || commands.isEmpty() - || rawCommands == null || rawCommands.isEmpty()) { + if (commands == null || commands.isEmpty()) { throw new IllegalArgumentException(MessageFormat.format( JGitText.get().pushCertificateInvalidField, "command")); //$NON-NLS-1$ @@ -123,7 +118,7 @@ public class PushCertificate { JGitText.get().pushCertificateInvalidSignature); } if (!signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE) - || !signature.endsWith(PushCertificateParser.END_SIGNATURE)) { + || !signature.endsWith(PushCertificateParser.END_SIGNATURE + '\n')) { throw new IllegalArgumentException( JGitText.get().pushCertificateInvalidSignature); } @@ -133,7 +128,6 @@ public class PushCertificate { this.nonce = nonce; this.nonceStatus = nonceStatus; this.commands = commands; - this.rawCommands = rawCommands; this.signature = signature; } @@ -209,14 +203,59 @@ public class PushCertificate { * @since 4.1 */ public String toText() { - return new StringBuilder() + StringBuilder sb = new StringBuilder() .append(VERSION).append(' ').append(version).append('\n') .append(PUSHER).append(' ').append(getPusher()) .append('\n') .append(PUSHEE).append(' ').append(pushee).append('\n') .append(NONCE).append(' ').append(nonce).append('\n') - .append('\n') - .append(rawCommands) - .toString(); + .append('\n'); + for (ReceiveCommand cmd : commands) { + sb.append(cmd.getOldId().name()) + .append(' ').append(cmd.getNewId().name()) + .append(' ').append(cmd.getRefName()).append('\n'); + } + return sb.toString(); + } + + @Override + public int hashCode() { + return signature.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PushCertificate)) { + return false; + } + PushCertificate p = (PushCertificate) o; + return version.equals(p.version) + && pusher.equals(p.pusher) + && Objects.equals(pushee, p.pushee) + && nonceStatus == p.nonceStatus + && signature.equals(p.signature) + && commandsEqual(this, p); + } + + private static boolean commandsEqual(PushCertificate c1, PushCertificate c2) { + if (c1.commands.size() != c2.commands.size()) { + return false; + } + for (int i = 0; i < c1.commands.size(); i++) { + ReceiveCommand cmd1 = c1.commands.get(i); + ReceiveCommand cmd2 = c2.commands.get(i); + if (!cmd1.getOldId().equals(cmd2.getOldId()) + || !cmd1.getNewId().equals(cmd2.getNewId()) + || !cmd1.getRefName().equals(cmd2.getRefName())) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return getClass().getSimpleName() + '[' + + toText() + signature + ']'; } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java index fea8f125e1..fd49e40c76 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java @@ -40,14 +40,15 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + package org.eclipse.jgit.transport; -import static org.eclipse.jgit.transport.BaseReceivePack.chomp; import static org.eclipse.jgit.transport.BaseReceivePack.parseCommand; import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT; import java.io.EOFException; import java.io.IOException; +import java.io.Reader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; @@ -58,6 +59,7 @@ import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.PushCertificate.NonceStatus; +import org.eclipse.jgit.util.IO; /** * Parser for signed push certificates. @@ -66,9 +68,9 @@ import org.eclipse.jgit.transport.PushCertificate.NonceStatus; */ public class PushCertificateParser { static final String BEGIN_SIGNATURE = - "-----BEGIN PGP SIGNATURE-----\n"; //$NON-NLS-1$ + "-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$ static final String END_SIGNATURE = - "-----END PGP SIGNATURE-----\n"; //$NON-NLS-1$ + "-----END PGP SIGNATURE-----"; //$NON-NLS-1$ static final String VERSION = "certificate version"; //$NON-NLS-1$ @@ -78,9 +80,81 @@ public class PushCertificateParser { static final String NONCE = "nonce"; //$NON-NLS-1$ + static final String END_CERT = "push-cert-end"; //$NON-NLS-1$ + private static final String VERSION_0_1 = "0.1"; //$NON-NLS-1$ - private static final String END_CERT = "push-cert-end\n"; //$NON-NLS-1$ + private static interface StringReader { + /** + * @return the next string from the input, up to an optional newline, with + * newline stripped if present + * + * @throws EOFException + * if EOF was reached. + * @throws IOException + * if an error occurred during reading. + */ + String read() throws EOFException, IOException; + } + + private static class PacketLineReader implements StringReader { + private final PacketLineIn pckIn; + + private PacketLineReader(PacketLineIn pckIn) { + this.pckIn = pckIn; + } + + @Override + public String read() throws IOException { + return pckIn.readString(); + } + } + + private static class StreamReader implements StringReader { + private final Reader reader; + + private StreamReader(Reader reader) { + this.reader = reader; + } + + @Override + public String read() throws IOException { + // Presize for a command containing 2 SHA-1s and some refname. + String line = IO.readLine(reader, 41 * 2 + 64); + if (line.isEmpty()) { + throw new EOFException(); + } else if (line.charAt(line.length() - 1) == '\n') { + line = line.substring(0, line.length() - 1); + } + return line; + } + } + + /** + * Parse a push certificate from a reader. + * <p> + * Differences from the {@link PacketLineIn} receiver methods: + * <ul> + * <li>Does not use pkt-line framing.</li> + * <li>Reads an entire cert in one call rather than depending on a loop in + * the caller.</li> + * <li>Does not assume a {@code "push-cert-end"} line.</li> + * </ul> + * + * @param r + * input reader; consumed only up until the end of the next + * signature in the input. + * @return the parsed certificate, or null if the reader was at EOF. + * @throws PackProtocolException + * if the certificate is malformed. + * @throws IOException + * if there was an error reading from the input. + * @since 4.1 + */ + public static PushCertificate fromReader(Reader r) + throws PackProtocolException, IOException { + return new PushCertificateParser().parse(r); + } private boolean received; private String version; @@ -110,11 +184,17 @@ public class PushCertificateParser { */ private final int nonceSlopLimit; + private final boolean enabled; private final NonceGenerator nonceGenerator; - private final List<ReceiveCommand> commands; - private final StringBuilder rawCommands; + private final List<ReceiveCommand> commands = new ArrayList<>(); - PushCertificateParser(Repository into, SignedPushConfig cfg) { + /** + * @param into + * destination repository for the push. + * @param cfg + * configuration for signed push. + */ + public PushCertificateParser(Repository into, SignedPushConfig cfg) { if (cfg != null) { nonceSlopLimit = cfg.getCertNonceSlopLimit(); nonceGenerator = cfg.getNonceGenerator(); @@ -123,8 +203,48 @@ public class PushCertificateParser { nonceGenerator = null; } db = into; - commands = new ArrayList<>(); - rawCommands = new StringBuilder(); + enabled = nonceGenerator != null; + } + + private PushCertificateParser() { + db = null; + nonceSlopLimit = 0; + nonceGenerator = null; + enabled = true; + } + + /** + * Parse a push certificate from a reader. + * + * @see #fromReader(Reader) + * @param r + * input reader; consumed only up until the end of the next + * signature in the input. + * @return the parsed certificate, or null if the reader was at EOF. + * @throws PackProtocolException + * if the certificate is malformed. + * @throws IOException + * if there was an error reading from the input. + * @since 4.1 + */ + public PushCertificate parse(Reader r) + throws PackProtocolException, IOException { + StreamReader reader = new StreamReader(r); + receiveHeader(reader, true); + String line; + try { + while (!(line = reader.read()).isEmpty()) { + if (line.equals(BEGIN_SIGNATURE)) { + receiveSignature(reader); + break; + } + addCommand(line); + } + } catch (EOFException e) { + // EOF reached, but might have been at a valid state. Let build call below + // sort it out. + } + return build(); } /** @@ -134,24 +254,24 @@ public class PushCertificateParser { * @since 4.1 */ public PushCertificate build() throws IOException { - if (!received || nonceGenerator == null) { + if (!received || !enabled) { return null; } try { return new PushCertificate(version, pusher, pushee, receivedNonce, - nonceStatus, Collections.unmodifiableList(commands), - rawCommands.toString(), signature); + nonceStatus, Collections.unmodifiableList(commands), signature); } catch (IllegalArgumentException e) { throw new IOException(e.getMessage(), e); } } /** - * @return if the server is configured to use signed pushes. + * @return if the repository is configured to use signed pushes in this + * context. * @since 4.0 */ public boolean enabled() { - return nonceGenerator != null; + return enabled; } /** @@ -175,14 +295,21 @@ public class PushCertificateParser { return sentNonce; } - private static String parseHeader(PacketLineIn pckIn, String header) + private static String parseHeader(StringReader reader, String header) + throws IOException { + return parseHeader(reader.read(), header); + } + + private static String parseHeader(String s, String header) throws IOException { - String s = pckIn.readString(); + if (s.isEmpty()) { + throw new EOFException(); + } if (s.length() <= header.length() || !s.startsWith(header) || s.charAt(header.length()) != ' ') { throw new PackProtocolException(MessageFormat.format( - JGitText.get().pushCertificateInvalidHeader, header)); + JGitText.get().pushCertificateInvalidField, header)); } return s.substring(header.length() + 1); } @@ -209,24 +336,42 @@ public class PushCertificateParser { */ public void receiveHeader(PacketLineIn pckIn, boolean stateless) throws IOException { - received = true; + receiveHeader(new PacketLineReader(pckIn), stateless); + } + + private void receiveHeader(StringReader reader, boolean stateless) + throws IOException { try { - version = parseHeader(pckIn, VERSION); + try { + version = parseHeader(reader, VERSION); + } catch (EOFException e) { + return; + } + received = true; if (!version.equals(VERSION_0_1)) { throw new PackProtocolException(MessageFormat.format( JGitText.get().pushCertificateInvalidFieldValue, VERSION, version)); } - String rawPusher = parseHeader(pckIn, PUSHER); + String rawPusher = parseHeader(reader, PUSHER); pusher = PushCertificateIdent.parse(rawPusher); if (pusher == null) { throw new PackProtocolException(MessageFormat.format( JGitText.get().pushCertificateInvalidFieldValue, PUSHER, rawPusher)); } - pushee = parseHeader(pckIn, PUSHEE); - receivedNonce = parseHeader(pckIn, NONCE); + String next = reader.read(); + if (next.startsWith(PUSHEE)) { + pushee = parseHeader(next, PUSHEE); + receivedNonce = parseHeader(reader, NONCE); + } else { + receivedNonce = parseHeader(next, NONCE); + } + nonceStatus = nonceGenerator != null + ? nonceGenerator.verify( + receivedNonce, sentNonce(), db, stateless, nonceSlopLimit) + : NonceStatus.UNSOLICITED; // An empty line. - if (!pckIn.readString().isEmpty()) { + if (!reader.read().isEmpty()) { throw new PackProtocolException( JGitText.get().pushCertificateInvalidHeader); } @@ -234,19 +379,15 @@ public class PushCertificateParser { throw new PackProtocolException( JGitText.get().pushCertificateInvalidHeader, eof); } - nonceStatus = nonceGenerator != null - ? nonceGenerator.verify( - receivedNonce, sentNonce(), db, stateless, nonceSlopLimit) - : NonceStatus.UNSOLICITED; } /** * Read the PGP signature. * <p> * This method assumes the line - * {@code "-----BEGIN PGP SIGNATURE-----\n"} has already been parsed, - * and continues parsing until an {@code "-----END PGP SIGNATURE-----\n"} is - * found, followed by {@code "push-cert-end\n"}. + * {@code "-----BEGIN PGP SIGNATURE-----"} has already been parsed, + * and continues parsing until an {@code "-----END PGP SIGNATURE-----"} is + * found, followed by {@code "push-cert-end"}. * * @param pckIn * where we read the signature from. @@ -255,18 +396,23 @@ public class PushCertificateParser { * @since 4.0 */ public void receiveSignature(PacketLineIn pckIn) throws IOException { + StringReader reader = new PacketLineReader(pckIn); + receiveSignature(reader); + if (!reader.read().equals(END_CERT)) { + throw new PackProtocolException( + JGitText.get().pushCertificateInvalidSignature); + } + } + + private void receiveSignature(StringReader reader) throws IOException { received = true; try { - StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE); + StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE).append('\n'); String line; - while (!(line = pckIn.readStringRaw()).equals(END_SIGNATURE)) { - sig.append(line); - } - signature = sig.append(END_SIGNATURE).toString(); - if (!pckIn.readStringRaw().equals(END_CERT)) { - throw new PackProtocolException( - JGitText.get().pushCertificateInvalidSignature); + while (!(line = reader.read()).equals(END_SIGNATURE)) { + sig.append(line).append('\n'); } + signature = sig.append(END_SIGNATURE).append('\n').toString(); } catch (EOFException eof) { throw new PackProtocolException( JGitText.get().pushCertificateInvalidSignature, eof); @@ -278,26 +424,23 @@ public class PushCertificateParser { * * @param cmd * the command. - * @param rawLine - * the exact line read from the wire that produced this - * command, including trailing newline if present. * @since 4.1 */ - public void addCommand(ReceiveCommand cmd, String rawLine) { + public void addCommand(ReceiveCommand cmd) { commands.add(cmd); - rawCommands.append(rawLine); } /** * Add a command to the signature. * - * @param rawLine - * the exact line read from the wire that produced this - * command, including trailing newline if present. + * @param line + * the line read from the wire that produced this + * command, with optional trailing newline already trimmed. + * @throws PackProtocolException + * if the raw line cannot be parsed to a command. * @since 4.0 */ - public void addCommand(String rawLine) { - commands.add(parseCommand(chomp(rawLine))); - rawCommands.append(rawLine); + public void addCommand(String line) throws PackProtocolException { + commands.add(parseCommand(line)); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java index c817c475aa..a9f5c9bf65 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java @@ -51,6 +51,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.text.MessageFormat; @@ -371,6 +372,74 @@ public class IO { return l; } + /** + * Read the next line from a reader. + * <p> + * Like {@link java.io.BufferedReader#readLine()}, but only treats {@code \n} + * as end-of-line, and includes the trailing newline. + * + * @param in + * the reader to read from. + * @param sizeHint + * hint for buffer sizing; 0 or negative for default. + * @return the next line from the input, always ending in {@code \n} unless + * EOF was reached. + * @throws IOException + * there was an error reading from the stream. + */ + public static String readLine(Reader in, int sizeHint) throws IOException { + if (in.markSupported()) { + if (sizeHint <= 0) { + sizeHint = 1024; + } + StringBuilder sb = new StringBuilder(sizeHint); + char[] buf = new char[sizeHint]; + while (true) { + in.mark(sizeHint); + int n = in.read(buf); + if (n < 0) { + in.reset(); + return sb.toString(); + } + for (int i = 0; i < n; i++) { + if (buf[i] == '\n') { + resetAndSkipFully(in, ++i); + sb.append(buf, 0, i); + return sb.toString(); + } + } + if (n > 0) { + sb.append(buf, 0, n); + } + resetAndSkipFully(in, n); + } + } else { + StringBuilder buf = sizeHint > 0 + ? new StringBuilder(sizeHint) + : new StringBuilder(); + int i; + while ((i = in.read()) != -1) { + char c = (char) i; + buf.append(c); + if (c == '\n') { + break; + } + } + return buf.toString(); + } + } + + private static void resetAndSkipFully(Reader fd, long toSkip) throws IOException { + fd.reset(); + while (toSkip > 0) { + long r = fd.skip(toSkip); + if (r <= 0) { + throw new EOFException(JGitText.get().shortSkipOfBlock); + } + toSkip -= r; + } + } + private IO() { // Don't create instances of a static only utility. } @@ -192,7 +192,7 @@ <maven.build.timestamp.format>yyyyMMddHHmm</maven.build.timestamp.format> <bundle-manifest>${project.build.directory}/META-INF/MANIFEST.MF</bundle-manifest> - <jgit-last-release-version>3.7.0.201502260915-r</jgit-last-release-version> + <jgit-last-release-version>4.0.0.201506090130-r</jgit-last-release-version> <jsch-version>0.1.51</jsch-version> <javaewah-version>0.7.9</javaewah-version> <junit-version>4.11</junit-version> |