From 5abd8a4feb5da689982c12b65faef34aabedeb26 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sat, 5 Dec 2020 21:55:29 +0100 Subject: [PATCH] Enable GpgSigner to also sign tags Factor out a common ObjectBuilder as super class of CommitBuilder and TagBuilder, and make the GpgSigner work on ObjectBuilder. In order not to break API, add the new method for signing an ObjectBuilder in a new interface GpgObjectSigner. The signature for a tag is just tacked onto the end of the tag message. The message of a signed tag must end in LF. Bug: 386908 Change-Id: I5e021e3c927f4051825cd7355b129113b949455e Signed-off-by: Thomas Wolf --- .../bc/internal/BouncyCastleGpgSigner.java | 16 +- .../eclipse/jgit/lib/CommitBuilderTest.java | 8 +- .../org/eclipse/jgit/lib/TagBuilderTest.java | 173 ++++++++++++++ .../eclipse/jgit/revwalk/RevTagParseTest.java | 119 ++++++++- .../eclipse/jgit/internal/JGitText.properties | 1 + .../org/eclipse/jgit/internal/JGitText.java | 1 + .../org/eclipse/jgit/lib/CommitBuilder.java | 184 ++------------ .../org/eclipse/jgit/lib/GpgObjectSigner.java | 59 +++++ .../org/eclipse/jgit/lib/ObjectBuilder.java | 225 ++++++++++++++++++ .../src/org/eclipse/jgit/lib/TagBuilder.java | 137 +++++++---- .../src/org/eclipse/jgit/revwalk/RevTag.java | 62 +++++ 11 files changed, 761 insertions(+), 224 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java index ea159c547..449c4a487 100644 --- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java +++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java @@ -38,6 +38,8 @@ import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.GpgSignature; import org.eclipse.jgit.lib.GpgSigner; +import org.eclipse.jgit.lib.GpgObjectSigner; +import org.eclipse.jgit.lib.ObjectBuilder; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.util.StringUtils; @@ -45,7 +47,8 @@ /** * GPG Signer using BouncyCastle library */ -public class BouncyCastleGpgSigner extends GpgSigner { +public class BouncyCastleGpgSigner extends GpgSigner + implements GpgObjectSigner { private static void registerBouncyCastleProviderIfNecessary() { if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { @@ -98,6 +101,13 @@ private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey, public void sign(@NonNull CommitBuilder commit, @Nullable String gpgSigningKey, @NonNull PersonIdent committer, CredentialsProvider credentialsProvider) throws CanceledException { + signObject(commit, gpgSigningKey, committer, credentialsProvider); + } + + @Override + public void signObject(@NonNull ObjectBuilder object, + @Nullable String gpgSigningKey, @NonNull PersonIdent committer, + CredentialsProvider credentialsProvider) throws CanceledException { try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt( credentialsProvider)) { BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey, @@ -158,10 +168,10 @@ public void sign(@NonNull CommitBuilder commit, ByteArrayOutputStream buffer = new ByteArrayOutputStream(); try (BCPGOutputStream out = new BCPGOutputStream( new ArmoredOutputStream(buffer))) { - signatureGenerator.update(commit.build()); + signatureGenerator.update(object.build()); signatureGenerator.generate().encode(out); } - commit.setGpgSignature(new GpgSignature(buffer.toByteArray())); + object.setGpgSignature(new GpgSignature(buffer.toByteArray())); } catch (PGPException | IOException | NoSuchAlgorithmException | NoSuchProviderException | URISyntaxException e) { throw new JGitInternalException(e.getMessage(), e); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java index dee58f9cf..2f1bada82 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, Salesforce. and others + * Copyright (C) 2018, 2020 Salesforce. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -53,7 +53,7 @@ public class CommitBuilderTest { private void assertGpgSignatureStringOutcome(String signature, String expectedOutcome) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); - CommitBuilder.writeGpgSignatureString(signature, out); + ObjectBuilder.writeMultiLineHeader(signature, out, true); String formatted_signature = new String(out.toByteArray(), US_ASCII); assertEquals(expectedOutcome, formatted_signature); } @@ -85,8 +85,8 @@ public void writeGpgSignatureString_failsForNonAscii() throws Exception { String signature = "Ü Ä"; IllegalArgumentException e = assertThrows( IllegalArgumentException.class, - () -> CommitBuilder.writeGpgSignatureString(signature, - new ByteArrayOutputStream())); + () -> ObjectBuilder.writeMultiLineHeader(signature, + new ByteArrayOutputStream(), true)); String message = MessageFormat.format(JGitText.get().notASCIIString, signature); assertEquals(message, e.getMessage()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java new file mode 100644 index 000000000..578602224 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2020 Thomas Wolf and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.util.RawParseUtils; +import org.junit.Test; + +public class TagBuilderTest { + + // @formatter:off + private static final String SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n" + + "Version: BCPG v1.60\n" + + "\n" + + "iQEcBAABCAAGBQJb9cVhAAoJEKX+6Axg/6TZeFsH/0CY0WX/z7U8+7S5giFX4wH4\n" + + "opvBwqyt6OX8lgNwTwBGHFNt8LdmDCCmKoq/XwkNi3ARVjLhe3gBcKXNoavvPk2Z\n" + + "gIg5ChevGkU4afWCOMLVEYnkCBGw2+86XhrK1P7gTHEk1Rd+Yv1ZRDJBY+fFO7yz\n" + + "uSBuF5RpEY2sJiIvp27Gub/rY3B5NTR/feO/z+b9oiP/fMUhpRwG5KuWUsn9NPjw\n" + + "3tvbgawYpU/2UnS+xnavMY4t2fjRYjsoxndPLb2MUX8X7vC7FgWLBlmI/rquLZVM\n" + + "IQEKkjnA+lhejjK1rv+ulq4kGZJFKGYWYYhRDwFg5PTkzhudhN2SGUq5Wxq1Eg4=\n" + + "=b9OI\n" + + "-----END PGP SIGNATURE-----"; + + // @formatter:on + + private static final String TAGGER_LINE = "A U. Thor 1218123387 +0700"; + + private static final PersonIdent TAGGER = RawParseUtils + .parsePersonIdent(TAGGER_LINE); + + @Test + public void testTagSimple() throws Exception { + TagBuilder t = new TagBuilder(); + t.setTag("sometag"); + t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT); + t.setEncoding(US_ASCII); + t.setMessage("Short message only"); + t.setTagger(TAGGER); + String tag = new String(t.build(), UTF_8); + String expected = "object 0000000000000000000000000000000000000000\n" + + "type commit\n" // + + "tag sometag\n" // + + "tagger " + TAGGER_LINE + '\n' // + + "encoding US-ASCII\n" // + + '\n' // + + "Short message only"; + assertEquals(expected, tag); + } + + @Test + public void testTagWithSignatureShortMessageEndsInLF() throws Exception { + TagBuilder t = new TagBuilder(); + t.setTag("sometag"); + t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT); + t.setEncoding(US_ASCII); + t.setMessage("Short message only\n"); + t.setTagger(TAGGER); + t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII))); + String tag = new String(t.build(), UTF_8); + String expected = "object 0000000000000000000000000000000000000000\n" + + "type commit\n" // + + "tag sometag\n" // + + "tagger " + TAGGER_LINE + '\n' // + + "encoding US-ASCII\n" // + + '\n' // + + "Short message only\n" // + + SIGNATURE + '\n'; + assertEquals(expected, tag); + } + + @Test + public void testTagWithSignatureMessageNoLF() { + TagBuilder t = new TagBuilder(); + t.setTag("sometag"); + t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT); + t.setEncoding(US_ASCII); + t.setMessage("A message\n\nthat does not end in LF"); + t.setTagger(TAGGER); + t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII))); + Throwable ex = assertThrows(Throwable.class, t::build); + assertEquals(JGitText.get().signedTagMessageNoLf, ex.getMessage()); + } + + @Test + public void testTagWithSignatureNoParagraphsMessage() throws Exception { + TagBuilder t = new TagBuilder(); + t.setTag("sometag"); + t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT); + t.setEncoding(US_ASCII); + t.setMessage("A strange\ntag message\n"); + t.setTagger(TAGGER); + t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII))); + String tag = new String(t.build(), UTF_8); + String expected = "object 0000000000000000000000000000000000000000\n" + + "type commit\n" // + + "tag sometag\n" // + + "tagger " + TAGGER_LINE + '\n' // + + "encoding US-ASCII\n" // + + '\n' // + + "A strange\ntag message\n" // + + SIGNATURE + '\n'; + assertEquals(expected, tag); + } + + @Test + public void testTagWithSignatureLongMessage() throws Exception { + TagBuilder t = new TagBuilder(); + t.setTag("sometag"); + t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT); + t.setMessage("Short message\n\nFollowed by explanations.\n"); + t.setTagger(TAGGER); + t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII))); + String tag = new String(t.build(), UTF_8); + String expected = "object 0000000000000000000000000000000000000000\n" + + "type commit\n" // + + "tag sometag\n" // + + "tagger " + TAGGER_LINE + '\n' // + + '\n' // + + "Short message\n\nFollowed by explanations.\n" // + + SIGNATURE + '\n'; + assertEquals(expected, tag); + } + + @Test + public void testTagWithSignatureEmptyMessage() throws Exception { + TagBuilder t = new TagBuilder(); + t.setTag("sometag"); + t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT); + t.setTagger(TAGGER); + t.setMessage(""); + String emptyMsg = new String(t.build(), UTF_8); + t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII))); + String tag = new String(t.build(), UTF_8); + String expected = "object 0000000000000000000000000000000000000000\n" + + "type commit\n" // + + "tag sometag\n" // + + "tagger " + TAGGER_LINE + '\n' // + + '\n'; + assertEquals(expected, emptyMsg); + assertEquals(expected + SIGNATURE + '\n', tag); + } + + @Test + public void testTagWithSignatureOnly() throws Exception { + TagBuilder t = new TagBuilder(); + t.setTag("sometag"); + t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT); + t.setTagger(TAGGER); + String emptyMsg = new String(t.build(), UTF_8); + t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII))); + String tag = new String(t.build(), UTF_8); + String expected = "object 0000000000000000000000000000000000000000\n" + + "type commit\n" // + + "tag sometag\n" // + + "tagger " + TAGGER_LINE + '\n' // + + '\n'; + assertEquals(expected, emptyMsg); + assertEquals(expected + SIGNATURE + '\n', tag); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java index b92a0726e..edddc33a2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008-2010, Google Inc. and others + * Copyright (C) 2008, 2020, Google Inc. and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -11,6 +11,7 @@ package org.eclipse.jgit.revwalk; import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -18,6 +19,7 @@ import static org.junit.Assert.assertSame; import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.junit.RepositoryTestCase; @@ -117,6 +119,7 @@ public void testParseAllFields() throws Exception { assertNotNull(c.getTagName()); assertEquals(name, c.getTagName()); assertEquals("", c.getFullMessage()); + assertNull(c.getRawGpgSignature()); final PersonIdent cTagger = c.getTaggerIdent(); assertNotNull(cTagger); @@ -128,13 +131,12 @@ public void testParseAllFields() throws Exception { public void testParseOldStyleNoTagger() throws Exception { final ObjectId treeId = id("9788669ad918b6fcce64af8882fc9a81cb6aba67"); final String name = "v1.2.3.4.5"; - final String message = "test\n" // - + "\n" // - + "-----BEGIN PGP SIGNATURE-----\n" // + final String fakeSignature = "-----BEGIN PGP SIGNATURE-----\n" // + "Version: GnuPG v1.4.1 (GNU/Linux)\n" // + "\n" // + "iD8DBQBC0b9oF3Y\n" // - + "-----END PGP SIGNATURE------n"; + + "-----END PGP SIGNATURE-----"; + final String message = "test\n\n" + fakeSignature + '\n'; final StringBuilder body = new StringBuilder(); @@ -167,6 +169,8 @@ public void testParseOldStyleNoTagger() throws Exception { assertEquals(name, c.getTagName()); assertEquals("test", c.getShortMessage()); assertEquals(message, c.getFullMessage()); + assertEquals(fakeSignature + '\n', + new String(c.getRawGpgSignature(), US_ASCII)); assertNull(c.getTaggerIdent()); } @@ -385,6 +389,108 @@ public void testParse_unsupportedEncoding() throws Exception { assertEquals("message\n", t.getFullMessage()); } + @Test + public void testParse_gpgSignature() throws Exception { + final String signature = "-----BEGIN PGP SIGNATURE-----\n\n" + + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n" + + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n" + + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n" + + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n" + + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n" + + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n" + + "=TClh\n" + "-----END PGP SIGNATURE-----"; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes(UTF_8)); + b.write("type tree\n".getBytes(UTF_8)); + b.write("tag v1.0\n".getBytes(UTF_8)); + b.write("tagger t 1218123387 +0700\n".getBytes(UTF_8)); + b.write('\n'); + b.write("message\n\n".getBytes(UTF_8)); + b.write(signature.getBytes(US_ASCII)); + b.write('\n'); + + RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + try (RevWalk rw = new RevWalk(db)) { + t.parseCanonical(rw, b.toByteArray()); + } + + assertEquals("t", t.getTaggerIdent().getName()); + assertEquals("message", t.getShortMessage()); + assertEquals("message\n\n" + signature + '\n', t.getFullMessage()); + String gpgSig = new String(t.getRawGpgSignature(), UTF_8); + assertEquals(signature + '\n', gpgSig); + } + + @Test + public void testParse_gpgSignature2() throws Exception { + final String signature = "-----BEGIN PGP SIGNATURE-----\n\n" + + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n" + + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n" + + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n" + + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n" + + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n" + + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n" + + "=TClh\n" + "-----END PGP SIGNATURE-----"; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes(UTF_8)); + b.write("type tree\n".getBytes(UTF_8)); + b.write("tag v1.0\n".getBytes(UTF_8)); + b.write("tagger t 1218123387 +0700\n".getBytes(UTF_8)); + b.write('\n'); + String message = "message\n\n" + signature.replace("xXXy", "aAAb") + + '\n'; + b.write(message.getBytes(UTF_8)); + b.write(signature.getBytes(US_ASCII)); + b.write('\n'); + + RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + try (RevWalk rw = new RevWalk(db)) { + t.parseCanonical(rw, b.toByteArray()); + } + + assertEquals("t", t.getTaggerIdent().getName()); + assertEquals("message", t.getShortMessage()); + assertEquals(message + signature + '\n', t.getFullMessage()); + String gpgSig = new String(t.getRawGpgSignature(), UTF_8); + assertEquals(signature + '\n', gpgSig); + } + + @Test + public void testParse_gpgSignature3() throws Exception { + final String signature = "-----BEGIN PGP SIGNATURE-----\n\n" + + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n" + + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n" + + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n" + + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n" + + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n" + + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n" + + "=TClh\n" + "-----END PGP SIGNATURE-----"; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes(UTF_8)); + b.write("type tree\n".getBytes(UTF_8)); + b.write("tag v1.0\n".getBytes(UTF_8)); + b.write("tagger t 1218123387 +0700\n".getBytes(UTF_8)); + b.write('\n'); + String message = "message\n\n-----BEGIN PGP SIGNATURE-----\n"; + b.write(message.getBytes(UTF_8)); + b.write(signature.getBytes(US_ASCII)); + b.write('\n'); + + RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + try (RevWalk rw = new RevWalk(db)) { + t.parseCanonical(rw, b.toByteArray()); + } + + assertEquals("t", t.getTaggerIdent().getName()); + assertEquals("message", t.getShortMessage()); + assertEquals(message + signature + '\n', t.getFullMessage()); + String gpgSig = new String(t.getRawGpgSignature(), UTF_8); + assertEquals(signature + '\n', gpgSig); + } + @Test public void testParse_NoMessage() throws Exception { final String msg = ""; @@ -447,7 +553,8 @@ public void testParse_GitStyleMessage() throws Exception { } @Test - public void testParse_PublicParseMethod() throws CorruptObjectException { + public void testParse_PublicParseMethod() + throws CorruptObjectException, UnsupportedEncodingException { TagBuilder src = new TagBuilder(); try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) { src.setObjectId(fmt.idFor(Constants.OBJ_TREE, new byte[] {}), 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 12902b900..6d15464d5 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -617,6 +617,7 @@ shortCompressedStreamAt=Short compressed stream at {0} shortReadOfBlock=Short read of block. shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section. shortSkipOfBlock=Short skip of block. +signedTagMessageNoLf=A non-empty message of a signed tag must end in LF. signingNotSupportedOnTag=Signing isn't supported on tag operations yet. signingServiceUnavailable=Signing service is not available similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100. 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 892657d5d..a7daed131 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -645,6 +645,7 @@ public static JGitText get() { /***/ public String shortReadOfBlock; /***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes; /***/ public String shortSkipOfBlock; + /***/ public String signedTagMessageNoLf; /***/ public String signingNotSupportedOnTag; /***/ public String signingServiceUnavailable; /***/ public String similarityScoreMustBeWithinBounds; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java index 4f93fda49..1665f051e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2007, Dave Watson - * Copyright (C) 2006-2007, Robin Rosenberg - * Copyright (C) 2006-2007, Shawn O. Pearce and others + * Copyright (C) 2006, 2007, Robin Rosenberg + * Copyright (C) 2006, 2020, Shawn O. Pearce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -16,14 +16,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; -import java.text.MessageFormat; import java.util.List; -import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.References; /** @@ -37,7 +34,7 @@ * and obtain a {@link org.eclipse.jgit.revwalk.RevCommit} instance by calling * {@link org.eclipse.jgit.revwalk.RevWalk#parseCommit(AnyObjectId)}. */ -public class CommitBuilder { +public class CommitBuilder extends ObjectBuilder { private static final ObjectId[] EMPTY_OBJECTID_LIST = new ObjectId[0]; private static final byte[] htree = Constants.encodeASCII("tree"); //$NON-NLS-1$ @@ -50,28 +47,17 @@ public class CommitBuilder { private static final byte[] hgpgsig = Constants.encodeASCII("gpgsig"); //$NON-NLS-1$ - private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$ - private ObjectId treeId; private ObjectId[] parentIds; - private PersonIdent author; - private PersonIdent committer; - private GpgSignature gpgSignature; - - private String message; - - private Charset encoding; - /** * Initialize an empty commit. */ public CommitBuilder() { parentIds = EMPTY_OBJECTID_LIST; - encoding = UTF_8; } /** @@ -98,8 +84,9 @@ public void setTreeId(AnyObjectId id) { * * @return the author of this commit (who wrote it). */ + @Override public PersonIdent getAuthor() { - return author; + return super.getAuthor(); } /** @@ -108,8 +95,9 @@ public PersonIdent getAuthor() { * @param newAuthor * the new author. Should not be null. */ + @Override public void setAuthor(PersonIdent newAuthor) { - author = newAuthor; + super.setAuthor(newAuthor); } /** @@ -131,38 +119,6 @@ public void setCommitter(PersonIdent newCommitter) { committer = newCommitter; } - /** - * Set the GPG signature of this commit. - *

- * Note, the signature set here will change the payload of the commit, i.e. - * the output of {@link #build()} will include the signature. Thus, the - * typical flow will be: - *

    - *
  1. call {@link #build()} without a signature set to obtain payload
  2. - *
  3. create {@link GpgSignature} from payload
  4. - *
  5. set {@link GpgSignature}
  6. - *
- *

- * - * @param newSignature - * the signature to set or null to unset - * @since 5.3 - */ - public void setGpgSignature(GpgSignature newSignature) { - gpgSignature = newSignature; - } - - /** - * Get the GPG signature of this commit. - * - * @return the GPG signature of this commit, maybe null if the - * commit is not to be signed - * @since 5.3 - */ - public GpgSignature getGpgSignature() { - return gpgSignature; - } - /** * Get the ancestors of this commit. * @@ -238,25 +194,6 @@ public void addParentId(AnyObjectId additionalParent) { } } - /** - * Get the complete commit message. - * - * @return the complete commit message. - */ - public String getMessage() { - return message; - } - - /** - * Set the commit message. - * - * @param newMessage - * the commit message. Should not be null. - */ - public void setMessage(String newMessage) { - message = newMessage; - } - /** * Set the encoding for the commit information. * @@ -267,37 +204,10 @@ public void setMessage(String newMessage) { */ @Deprecated public void setEncoding(String encodingName) { - encoding = Charset.forName(encodingName); + setEncoding(Charset.forName(encodingName)); } - /** - * Set the encoding for the commit information. - * - * @param enc - * the encoding to use. - */ - public void setEncoding(Charset enc) { - encoding = enc; - } - - /** - * Get the encoding that should be used for the commit message text. - * - * @return the encoding that should be used for the commit message text. - */ - public Charset getEncoding() { - return encoding; - } - - /** - * Format this builder's state as a commit object. - * - * @return this object in the canonical commit format, suitable for storage - * in a repository. - * @throws java.io.UnsupportedEncodingException - * the encoding specified by {@link #getEncoding()} is not - * supported by this Java runtime. - */ + @Override public byte[] build() throws UnsupportedEncodingException { ByteArrayOutputStream os = new ByteArrayOutputStream(); OutputStreamWriter w = new OutputStreamWriter(os, getEncoding()); @@ -326,19 +236,16 @@ public byte[] build() throws UnsupportedEncodingException { w.flush(); os.write('\n'); - if (getGpgSignature() != null) { + GpgSignature signature = getGpgSignature(); + if (signature != null) { os.write(hgpgsig); os.write(' '); - writeGpgSignatureString(getGpgSignature().toExternalString(), os); + writeMultiLineHeader(signature.toExternalString(), os, + true); os.write('\n'); } - if (!References.isSameObject(getEncoding(), UTF_8)) { - os.write(hencoding); - os.write(' '); - os.write(Constants.encodeASCII(getEncoding().name())); - os.write('\n'); - } + writeEncoding(getEncoding(), os); os.write('\n'); @@ -355,58 +262,6 @@ public byte[] build() throws UnsupportedEncodingException { return os.toByteArray(); } - /** - * Writes signature to output as per gpgsig - * header. - *

- * CRLF and CR will be sanitized to LF and signature will have a hanging - * indent of one space starting with line two. A trailing line break is - * not written; the caller is supposed to terminate the GPG - * signature header by writing a single newline. - *

- * - * @param in - * signature string with line breaks - * @param out - * output stream - * @throws IOException - * thrown by the output stream - * @throws IllegalArgumentException - * if the signature string contains non 7-bit ASCII chars - */ - static void writeGpgSignatureString(String in, OutputStream out) - throws IOException, IllegalArgumentException { - int length = in.length(); - for (int i = 0; i < length; ++i) { - char ch = in.charAt(i); - switch (ch) { - case '\r': - if (i + 1 < length && in.charAt(i + 1) == '\n') { - ++i; - } - if (i + 1 < length) { - out.write('\n'); - out.write(' '); - } - break; - case '\n': - if (i + 1 < length) { - out.write('\n'); - out.write(' '); - } - break; - default: - // sanity check - if (ch > 127) - throw new IllegalArgumentException(MessageFormat - .format(JGitText.get().notASCIIString, in)); - out.write(ch); - break; - } - } - } - /** * Format this builder's state as a commit object. * @@ -439,7 +294,7 @@ public String toString() { } r.append("author "); - r.append(author != null ? author.toString() : "NOT_SET"); + r.append(getAuthor() != null ? getAuthor().toString() : "NOT_SET"); r.append("\n"); r.append("committer "); @@ -447,17 +302,20 @@ public String toString() { r.append("\n"); r.append("gpgSignature "); - r.append(gpgSignature != null ? gpgSignature.toString() : "NOT_SET"); + GpgSignature signature = getGpgSignature(); + r.append(signature != null ? signature.toString() + : "NOT_SET"); r.append("\n"); - if (encoding != null && !References.isSameObject(encoding, UTF_8)) { + Charset encoding = getEncoding(); + if (!References.isSameObject(encoding, UTF_8)) { r.append("encoding "); r.append(encoding.name()); r.append("\n"); } r.append("\n"); - r.append(message != null ? message : ""); + r.append(getMessage() != null ? getMessage() : ""); r.append("}"); return r.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java new file mode 100644 index 000000000..6fb767774 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 Thomas Wolf and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.api.errors.CanceledException; +import org.eclipse.jgit.transport.CredentialsProvider; + +/** + * Creates GPG signatures for Git objects. + * + * @since 5.11 + */ +public interface GpgObjectSigner { + + /** + * Signs the specified object. + * + *

+ * Implementors should obtain the payload for signing from the specified + * object via {@link ObjectBuilder#build()} and create a proper + * {@link GpgSignature}. The generated signature must be set on the + * specified {@code object} (see + * {@link ObjectBuilder#setGpgSignature(GpgSignature)}). + *

+ *

+ * Any existing signature on the object must be discarded prior obtaining + * the payload via {@link ObjectBuilder#build()}. + *

+ * + * @param object + * the object to sign (must not be {@code null} and must be + * complete to allow proper calculation of payload) + * @param gpgSigningKey + * the signing key to locate (passed as is to the GPG signing + * tool as is; eg., value of user.signingkey) + * @param committer + * the signing identity (to help with key lookup in case signing + * key is not specified) + * @param credentialsProvider + * provider to use when querying for signing key credentials (eg. + * passphrase) + * @throws CanceledException + * when signing was canceled (eg., user aborted when entering + * passphrase) + */ + void signObject(@NonNull ObjectBuilder object, + @Nullable String gpgSigningKey, @NonNull PersonIdent committer, + CredentialsProvider credentialsProvider) throws CanceledException; + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java new file mode 100644 index 000000000..4b7054f72 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2020, Thomas Wolf and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.lib; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.Objects; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.util.References; + +/** + * Common base class for {@link CommitBuilder} and {@link TagBuilder}. + * + * @since 5.11 + */ +public abstract class ObjectBuilder { + + /** Byte representation of "encoding". */ + private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$ + + private PersonIdent author; + + private GpgSignature gpgSignature; + + private String message; + + private Charset encoding = StandardCharsets.UTF_8; + + /** + * Retrieves the author of this object. + * + * @return the author of this object, or {@code null} if not set yet + */ + protected PersonIdent getAuthor() { + return author; + } + + /** + * Sets the author (name, email address, and date) of this object. + * + * @param newAuthor + * the new author, must be non-{@code null} + */ + protected void setAuthor(PersonIdent newAuthor) { + author = Objects.requireNonNull(newAuthor); + } + + /** + * Sets the GPG signature of this object. + *

+ * Note, the signature set here will change the payload of the object, i.e. + * the output of {@link #build()} will include the signature. Thus, the + * typical flow will be: + *

    + *
  1. call {@link #build()} without a signature set to obtain payload
  2. + *
  3. create {@link GpgSignature} from payload
  4. + *
  5. set {@link GpgSignature}
  6. + *
+ *

+ * + * @param gpgSignature + * the signature to set or {@code null} to unset + * @since 5.3 + */ + public void setGpgSignature(@Nullable GpgSignature gpgSignature) { + this.gpgSignature = gpgSignature; + } + + /** + * Retrieves the GPG signature of this object. + * + * @return the GPG signature of this object, or {@code null} if the object + * is not signed + * @since 5.3 + */ + @Nullable + public GpgSignature getGpgSignature() { + return gpgSignature; + } + + /** + * Retrieves the complete message of the object. + * + * @return the complete message; can be {@code null}. + */ + @Nullable + public String getMessage() { + return message; + } + + /** + * Sets the message (commit message, or message of an annotated tag). + * + * @param message + * the message. + */ + public void setMessage(@Nullable String message) { + this.message = message; + } + + /** + * Retrieves the encoding that should be used for the message text. + * + * @return the encoding that should be used for the message text. + */ + @NonNull + public Charset getEncoding() { + return encoding; + } + + /** + * Sets the encoding for the object message. + * + * @param encoding + * the encoding to use. + */ + public void setEncoding(@NonNull Charset encoding) { + this.encoding = encoding; + } + + /** + * Format this builder's state as a git object. + * + * @return this object in the canonical git format, suitable for storage in + * a repository. + * @throws java.io.UnsupportedEncodingException + * the encoding specified by {@link #getEncoding()} is not + * supported by this Java runtime. + */ + @NonNull + public abstract byte[] build() throws UnsupportedEncodingException; + + /** + * Writes signature to output as per gpgsig + * header. + *

+ * CRLF and CR will be sanitized to LF and signature will have a hanging + * indent of one space starting with line two. A trailing line break is + * not written; the caller is supposed to terminate the GPG + * signature header by writing a single newline. + *

+ * + * @param in + * signature string with line breaks + * @param out + * output stream + * @param enforceAscii + * whether to throw {@link IllegalArgumentException} if non-ASCII + * characters are encountered + * @throws IOException + * thrown by the output stream + * @throws IllegalArgumentException + * if the signature string contains non 7-bit ASCII chars and + * {@code enforceAscii == true} + */ + static void writeMultiLineHeader(@NonNull String in, + @NonNull OutputStream out, boolean enforceAscii) + throws IOException, IllegalArgumentException { + int length = in.length(); + for (int i = 0; i < length; ++i) { + char ch = in.charAt(i); + switch (ch) { + case '\r': + if (i + 1 < length && in.charAt(i + 1) == '\n') { + ++i; + } + if (i + 1 < length) { + out.write('\n'); + out.write(' '); + } + break; + case '\n': + if (i + 1 < length) { + out.write('\n'); + out.write(' '); + } + break; + default: + // sanity check + if (ch > 127 && enforceAscii) + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().notASCIIString, in)); + out.write(ch); + break; + } + } + } + + /** + * Writes an "encoding" header. + * + * @param encoding + * to write + * @param out + * to write to + * @throws IOException + * if writing fails + */ + static void writeEncoding(@NonNull Charset encoding, + @NonNull OutputStream out) throws IOException { + if (!References.isSameObject(encoding, UTF_8)) { + out.write(hencoding); + out.write(' '); + out.write(Constants.encodeASCII(encoding.name())); + out.write('\n'); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java index 71f01150c..facb4a54b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java @@ -1,7 +1,7 @@ /* - * Copyright (C) 2006-2008, Robin Rosenberg + * Copyright (C) 2006, 2008, Robin Rosenberg * Copyright (C) 2008, Shawn O. Pearce - * Copyright (C) 2010, Chris Aniszczyk and others + * Copyright (C) 2010, 2020, Chris Aniszczyk and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -17,8 +17,13 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.util.References; /** * Mutable builder to construct an annotated tag recording a project state. @@ -30,17 +35,22 @@ * and obtain a {@link org.eclipse.jgit.revwalk.RevTag} instance by calling * {@link org.eclipse.jgit.revwalk.RevWalk#parseTag(AnyObjectId)}. */ -public class TagBuilder { +public class TagBuilder extends ObjectBuilder { + + private static final byte[] hobject = Constants.encodeASCII("object"); //$NON-NLS-1$ + + private static final byte[] htype = Constants.encodeASCII("type"); //$NON-NLS-1$ + + private static final byte[] htag = Constants.encodeASCII("tag"); //$NON-NLS-1$ + + private static final byte[] htagger = Constants.encodeASCII("tagger"); //$NON-NLS-1$ + private ObjectId object; private int type = Constants.OBJ_BAD; private String tag; - private PersonIdent tagger; - - private String message; - /** * Get the type of object this tag refers to. * @@ -109,7 +119,7 @@ public void setTag(String shortName) { * @return creator of this tag. May be null. */ public PersonIdent getTagger() { - return tagger; + return getAuthor(); } /** @@ -119,26 +129,7 @@ public PersonIdent getTagger() { * the creator. May be null. */ public void setTagger(PersonIdent taggerIdent) { - tagger = taggerIdent; - } - - /** - * Get the complete commit message. - * - * @return the complete commit message. - */ - public String getMessage() { - return message; - } - - /** - * Set the tag's message. - * - * @param newMessage - * the tag's message. - */ - public void setMessage(String newMessage) { - message = newMessage; + setAuthor(taggerIdent); } /** @@ -147,31 +138,65 @@ public void setMessage(String newMessage) { * @return this object in the canonical annotated tag format, suitable for * storage in a repository. */ - public byte[] build() { + @Override + public byte[] build() throws UnsupportedEncodingException { ByteArrayOutputStream os = new ByteArrayOutputStream(); try (OutputStreamWriter w = new OutputStreamWriter(os, - UTF_8)) { - w.write("object "); //$NON-NLS-1$ - getObjectId().copyTo(w); - w.write('\n'); + getEncoding())) { - w.write("type "); //$NON-NLS-1$ - w.write(Constants.typeString(getObjectType())); - w.write("\n"); //$NON-NLS-1$ + os.write(hobject); + os.write(' '); + getObjectId().copyTo(os); + os.write('\n'); - w.write("tag "); //$NON-NLS-1$ + os.write(htype); + os.write(' '); + os.write(Constants + .encodeASCII(Constants.typeString(getObjectType()))); + os.write('\n'); + + os.write(htag); + os.write(' '); w.write(getTag()); - w.write("\n"); //$NON-NLS-1$ + w.flush(); + os.write('\n'); if (getTagger() != null) { - w.write("tagger "); //$NON-NLS-1$ + os.write(htagger); + os.write(' '); w.write(getTagger().toExternalString()); - w.write('\n'); + w.flush(); + os.write('\n'); } - w.write('\n'); - if (getMessage() != null) - w.write(getMessage()); + writeEncoding(getEncoding(), os); + + os.write('\n'); + String msg = getMessage(); + if (msg != null) { + w.write(msg); + w.flush(); + } + + GpgSignature signature = getGpgSignature(); + if (signature != null) { + if (msg != null && !msg.isEmpty() && !msg.endsWith("\n")) { //$NON-NLS-1$ + // If signed, the message *must* end with a linefeed + // character, otherwise signature verification will fail. + // (The signature will have been computed over the payload + // containing the message without LF, but will be verified + // against a payload with the LF.) The signature must start + // on a new line. + throw new JGitInternalException( + JGitText.get().signedTagMessageNoLf); + } + String externalForm = signature.toExternalString(); + w.write(externalForm); + w.flush(); + if (!externalForm.endsWith("\n")) { //$NON-NLS-1$ + os.write('\n'); + } + } } catch (IOException err) { // This should never occur, the only way to get it above is // for the ByteArrayOutputStream to throw, but it doesn't. @@ -185,10 +210,17 @@ public byte[] build() { * Format this builder's state as an annotated tag object. * * @return this object in the canonical annotated tag format, suitable for - * storage in a repository. + * storage in a repository, or {@code null} if the tag cannot be + * encoded + * @deprecated since 5.11; use {@link #build()} instead */ + @Deprecated public byte[] toByteArray() { - return build(); + try { + return build(); + } catch (UnsupportedEncodingException e) { + return null; + } } /** {@inheritDoc} */ @@ -211,14 +243,23 @@ public String toString() { r.append(tag != null ? tag : "NOT_SET"); r.append("\n"); - if (tagger != null) { + if (getTagger() != null) { r.append("tagger "); - r.append(tagger); + r.append(getTagger()); + r.append("\n"); + } + + Charset encoding = getEncoding(); + if (!References.isSameObject(encoding, UTF_8)) { + r.append("encoding "); + r.append(encoding.name()); r.append("\n"); } r.append("\n"); - r.append(message != null ? message : ""); + r.append(getMessage() != null ? getMessage() : ""); + GpgSignature signature = getGpgSignature(); + r.append(signature != null ? signature.toExternalString() : ""); r.append("}"); return r.toString(); } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java index cac257199..3bcdfafea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java @@ -18,7 +18,9 @@ import java.nio.charset.Charset; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; +import java.util.Arrays; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -35,6 +37,10 @@ * An annotated tag. */ public class RevTag extends RevObject { + + private static final byte[] hSignature = Constants + .encodeASCII("-----BEGIN PGP SIGNATURE-----"); //$NON-NLS-1$ + /** * Parse an annotated tag from its canonical format. * @@ -171,6 +177,62 @@ public final PersonIdent getTaggerIdent() { return RawParseUtils.parsePersonIdent(raw, nameB); } + private static int nextStart(byte[] prefix, byte[] buffer, int from) { + int stop = buffer.length - prefix.length + 1; + int ptr = from; + if (ptr > 0) { + ptr = RawParseUtils.nextLF(buffer, ptr - 1); + } + while (ptr < stop) { + int lineStart = ptr; + boolean found = true; + for (byte element : prefix) { + if (element != buffer[ptr++]) { + found = false; + break; + } + } + if (found) { + return lineStart; + } + do { + ptr = RawParseUtils.nextLF(buffer, ptr); + } while (ptr < stop && buffer[ptr] == '\n'); + } + return -1; + } + + /** + * Parse the GPG signature from the raw buffer. + * + * @return contents of the GPG signature; {@code null} if the tag was not + * signed. + * @since 5.11 + */ + @Nullable + public final byte[] getRawGpgSignature() { + byte[] raw = buffer; + int msgB = RawParseUtils.tagMessage(raw, 0); + if (msgB < 0) { + return null; + } + // Find the last signature start and return the rest + int start = nextStart(hSignature, raw, msgB); + if (start < 0) { + return null; + } + int next = RawParseUtils.nextLF(raw, start); + while (next < raw.length) { + int newStart = nextStart(hSignature, raw, next); + if (newStart < 0) { + break; + } + start = newStart; + next = RawParseUtils.nextLF(raw, start); + } + return Arrays.copyOfRange(raw, start, raw.length); + } + /** * Parse the complete tag message and decode it to a string. *