blob: 819c8ac730f81f47f5df0b73803f4bc58a86ef7c [file] [log] [blame]
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.notedb;
import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.Comment.Key;
import com.google.gerrit.reviewdb.client.CommentRange;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.config.GerritServerId;
import com.google.inject.Inject;
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.util.GitDateParser;
import org.eclipse.jgit.util.MutableInteger;
import org.eclipse.jgit.util.QuotedString;
import org.eclipse.jgit.util.RawParseUtils;
public class LegacyChangeNoteRead {
private final String serverId;
@Inject
public LegacyChangeNoteRead(@GerritServerId String serverId) {
this.serverId = serverId;
}
public Account.Id parseIdent(PersonIdent ident, Change.Id changeId)
throws ConfigInvalidException {
return NoteDbUtil.parseIdent(ident, serverId)
.orElseThrow(
() ->
parseException(
changeId,
"invalid identity, expected <id>@%s: %s",
serverId,
ident.getEmailAddress()));
}
private static boolean match(byte[] note, MutableInteger p, byte[] expected) {
int m = RawParseUtils.match(note, p.value, expected);
return m == p.value + expected.length;
}
public List<Comment> parseNote(byte[] note, MutableInteger p, Change.Id changeId)
throws ConfigInvalidException {
if (p.value >= note.length) {
return ImmutableList.of();
}
Set<Key> seen = new HashSet<>();
List<Comment> result = new ArrayList<>();
int sizeOfNote = note.length;
byte[] psb = ChangeNoteUtil.PATCH_SET.getBytes(UTF_8);
byte[] bpsb = ChangeNoteUtil.BASE_PATCH_SET.getBytes(UTF_8);
byte[] bpn = ChangeNoteUtil.PARENT_NUMBER.getBytes(UTF_8);
RevId revId = new RevId(parseStringField(note, p, changeId, ChangeNoteUtil.REVISION));
String fileName = null;
PatchSet.Id psId = null;
boolean isForBase = false;
Integer parentNumber = null;
while (p.value < sizeOfNote) {
boolean matchPs = match(note, p, psb);
boolean matchBase = match(note, p, bpsb);
if (matchPs) {
fileName = null;
psId = parsePsId(note, p, changeId, ChangeNoteUtil.PATCH_SET);
isForBase = false;
} else if (matchBase) {
fileName = null;
psId = parsePsId(note, p, changeId, ChangeNoteUtil.BASE_PATCH_SET);
isForBase = true;
if (match(note, p, bpn)) {
parentNumber = parseParentNumber(note, p, changeId);
}
} else if (psId == null) {
throw parseException(
changeId,
"missing %s or %s header",
ChangeNoteUtil.PATCH_SET,
ChangeNoteUtil.BASE_PATCH_SET);
}
Comment c = parseComment(note, p, fileName, psId, revId, isForBase, parentNumber);
fileName = c.key.filename;
if (!seen.add(c.key)) {
throw parseException(changeId, "multiple comments for %s in note", c.key);
}
result.add(c);
}
return result;
}
private Comment parseComment(
byte[] note,
MutableInteger curr,
String currentFileName,
PatchSet.Id psId,
RevId revId,
boolean isForBase,
Integer parentNumber)
throws ConfigInvalidException {
Change.Id changeId = psId.getParentKey();
// Check if there is a new file.
boolean newFile =
(RawParseUtils.match(note, curr.value, ChangeNoteUtil.FILE.getBytes(UTF_8))) != -1;
if (newFile) {
// If so, parse the new file name.
currentFileName = parseFilename(note, curr, changeId);
} else if (currentFileName == null) {
throw parseException(changeId, "could not parse %s", ChangeNoteUtil.FILE);
}
CommentRange range = parseCommentRange(note, curr);
if (range == null) {
throw parseException(changeId, "could not parse %s", ChangeNoteUtil.COMMENT_RANGE);
}
Timestamp commentTime = parseTimestamp(note, curr, changeId);
Account.Id aId = parseAuthor(note, curr, changeId, ChangeNoteUtil.AUTHOR);
boolean hasRealAuthor =
(RawParseUtils.match(note, curr.value, ChangeNoteUtil.REAL_AUTHOR.getBytes(UTF_8))) != -1;
Account.Id raId = null;
if (hasRealAuthor) {
raId = parseAuthor(note, curr, changeId, ChangeNoteUtil.REAL_AUTHOR);
}
boolean hasParent =
(RawParseUtils.match(note, curr.value, ChangeNoteUtil.PARENT.getBytes(UTF_8))) != -1;
String parentUUID = null;
boolean unresolved = false;
if (hasParent) {
parentUUID = parseStringField(note, curr, changeId, ChangeNoteUtil.PARENT);
}
boolean hasUnresolved =
(RawParseUtils.match(note, curr.value, ChangeNoteUtil.UNRESOLVED.getBytes(UTF_8))) != -1;
if (hasUnresolved) {
unresolved = parseBooleanField(note, curr, changeId, ChangeNoteUtil.UNRESOLVED);
}
String uuid = parseStringField(note, curr, changeId, ChangeNoteUtil.UUID);
boolean hasTag =
(RawParseUtils.match(note, curr.value, ChangeNoteUtil.TAG.getBytes(UTF_8))) != -1;
String tag = null;
if (hasTag) {
tag = parseStringField(note, curr, changeId, ChangeNoteUtil.TAG);
}
int commentLength = parseCommentLength(note, curr, changeId);
String message = RawParseUtils.decode(UTF_8, note, curr.value, curr.value + commentLength);
checkResult(message, "message contents", changeId);
Comment c =
new Comment(
new Comment.Key(uuid, currentFileName, psId.get()),
aId,
commentTime,
isForBase ? (short) (parentNumber == null ? 0 : -parentNumber) : (short) 1,
message,
serverId,
unresolved);
c.lineNbr = range.getEndLine();
c.parentUuid = parentUUID;
c.tag = tag;
c.setRevId(revId);
if (raId != null) {
c.setRealAuthor(raId);
}
if (range.getStartCharacter() != -1) {
c.setRange(range);
}
curr.value = RawParseUtils.nextLF(note, curr.value + commentLength);
curr.value = RawParseUtils.nextLF(note, curr.value);
return c;
}
private static String parseStringField(
byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
throws ConfigInvalidException {
int endOfLine = RawParseUtils.nextLF(note, curr.value);
checkHeaderLineFormat(note, curr, fieldName, changeId);
int startOfField = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
curr.value = endOfLine;
return RawParseUtils.decode(UTF_8, note, startOfField, endOfLine - 1);
}
/**
* @return a comment range. If the comment range line in the note only has one number, we return a
* CommentRange with that one number as the end line and the other fields as -1. If the
* comment range line in the note contains a whole comment range, then we return a
* CommentRange with all fields set. If the line is not correctly formatted, return null.
*/
private static CommentRange parseCommentRange(byte[] note, MutableInteger ptr) {
CommentRange range = new CommentRange(-1, -1, -1, -1);
int last = ptr.value;
int startLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
if (ptr.value == last) {
return null;
} else if (note[ptr.value] == '\n') {
range.setEndLine(startLine);
ptr.value += 1;
return range;
} else if (note[ptr.value] == ':') {
range.setStartLine(startLine);
ptr.value += 1;
} else {
return null;
}
last = ptr.value;
int startChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
if (ptr.value == last) {
return null;
} else if (note[ptr.value] == '-') {
range.setStartCharacter(startChar);
ptr.value += 1;
} else {
return null;
}
last = ptr.value;
int endLine = RawParseUtils.parseBase10(note, ptr.value, ptr);
if (ptr.value == last) {
return null;
} else if (note[ptr.value] == ':') {
range.setEndLine(endLine);
ptr.value += 1;
} else {
return null;
}
last = ptr.value;
int endChar = RawParseUtils.parseBase10(note, ptr.value, ptr);
if (ptr.value == last) {
return null;
} else if (note[ptr.value] == '\n') {
range.setEndCharacter(endChar);
ptr.value += 1;
} else {
return null;
}
return range;
}
private static PatchSet.Id parsePsId(
byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
throws ConfigInvalidException {
checkHeaderLineFormat(note, curr, fieldName, changeId);
int startOfPsId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
MutableInteger i = new MutableInteger();
int patchSetId = RawParseUtils.parseBase10(note, startOfPsId, i);
int endOfLine = RawParseUtils.nextLF(note, curr.value);
if (i.value != endOfLine - 1) {
throw parseException(changeId, "could not parse %s", fieldName);
}
checkResult(patchSetId, "patchset id", changeId);
curr.value = endOfLine;
return new PatchSet.Id(changeId, patchSetId);
}
private static Integer parseParentNumber(byte[] note, MutableInteger curr, Change.Id changeId)
throws ConfigInvalidException {
checkHeaderLineFormat(note, curr, ChangeNoteUtil.PARENT_NUMBER, changeId);
int start = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
MutableInteger i = new MutableInteger();
int parentNumber = RawParseUtils.parseBase10(note, start, i);
int endOfLine = RawParseUtils.nextLF(note, curr.value);
if (i.value != endOfLine - 1) {
throw parseException(changeId, "could not parse %s", ChangeNoteUtil.PARENT_NUMBER);
}
checkResult(parentNumber, "parent number", changeId);
curr.value = endOfLine;
return Integer.valueOf(parentNumber);
}
private static String parseFilename(byte[] note, MutableInteger curr, Change.Id changeId)
throws ConfigInvalidException {
checkHeaderLineFormat(note, curr, ChangeNoteUtil.FILE, changeId);
int startOfFileName = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
int endOfLine = RawParseUtils.nextLF(note, curr.value);
curr.value = endOfLine;
curr.value = RawParseUtils.nextLF(note, curr.value);
return QuotedString.GIT_PATH.dequote(
RawParseUtils.decode(UTF_8, note, startOfFileName, endOfLine - 1));
}
private static Timestamp parseTimestamp(byte[] note, MutableInteger curr, Change.Id changeId)
throws ConfigInvalidException {
int endOfLine = RawParseUtils.nextLF(note, curr.value);
Timestamp commentTime;
String dateString = RawParseUtils.decode(UTF_8, note, curr.value, endOfLine - 1);
try {
commentTime = new Timestamp(GitDateParser.parse(dateString, null, Locale.US).getTime());
} catch (ParseException e) {
throw new ConfigInvalidException("could not parse comment timestamp", e);
}
curr.value = endOfLine;
return checkResult(commentTime, "comment timestamp", changeId);
}
private Account.Id parseAuthor(
byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
throws ConfigInvalidException {
checkHeaderLineFormat(note, curr, fieldName, changeId);
int startOfAccountId = RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
PersonIdent ident = RawParseUtils.parsePersonIdent(note, startOfAccountId);
Account.Id aId = parseIdent(ident, changeId);
curr.value = RawParseUtils.nextLF(note, curr.value);
return checkResult(aId, fieldName, changeId);
}
private static int parseCommentLength(byte[] note, MutableInteger curr, Change.Id changeId)
throws ConfigInvalidException {
checkHeaderLineFormat(note, curr, ChangeNoteUtil.LENGTH, changeId);
int startOfLength = RawParseUtils.endOfFooterLineKey(note, curr.value) + 1;
MutableInteger i = new MutableInteger();
i.value = startOfLength;
int commentLength = RawParseUtils.parseBase10(note, startOfLength, i);
if (i.value == startOfLength) {
throw parseException(changeId, "could not parse %s", ChangeNoteUtil.LENGTH);
}
int endOfLine = RawParseUtils.nextLF(note, curr.value);
if (i.value != endOfLine - 1) {
throw parseException(changeId, "could not parse %s", ChangeNoteUtil.LENGTH);
}
curr.value = endOfLine;
return checkResult(commentLength, "comment length", changeId);
}
private boolean parseBooleanField(
byte[] note, MutableInteger curr, Change.Id changeId, String fieldName)
throws ConfigInvalidException {
String str = parseStringField(note, curr, changeId, fieldName);
if ("true".equalsIgnoreCase(str)) {
return true;
} else if ("false".equalsIgnoreCase(str)) {
return false;
}
throw parseException(changeId, "invalid boolean for %s: %s", fieldName, str);
}
private static <T> T checkResult(T o, String fieldName, Change.Id changeId)
throws ConfigInvalidException {
if (o == null) {
throw parseException(changeId, "could not parse %s", fieldName);
}
return o;
}
private static int checkResult(int i, String fieldName, Change.Id changeId)
throws ConfigInvalidException {
if (i <= 0) {
throw parseException(changeId, "could not parse %s", fieldName);
}
return i;
}
private static void checkHeaderLineFormat(
byte[] note, MutableInteger curr, String fieldName, Change.Id changeId)
throws ConfigInvalidException {
boolean correct = RawParseUtils.match(note, curr.value, fieldName.getBytes(UTF_8)) != -1;
int p = curr.value + fieldName.length();
correct &= (p < note.length && note[p] == ':');
p++;
correct &= (p < note.length && note[p] == ' ');
if (!correct) {
throw parseException(changeId, "could not parse %s", fieldName);
}
}
}