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. *