/* * Copyright (C) 2010, 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 * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.notes; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; import static org.eclipse.jgit.lib.Constants.encodeASCII; import static org.eclipse.jgit.lib.FileMode.TREE; import static org.eclipse.jgit.util.RawParseUtils.parseHexInt4; import java.io.IOException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.treewalk.CanonicalTreeParser; /** Custom tree parser to select note bucket type and load it. */ final class NoteParser extends CanonicalTreeParser { /** * Parse a tree object into a {@link NoteBucket} instance. * * The type of note tree is automatically detected by examining the items * within the tree, and allocating the proper storage type based on the * first note-like entry encountered. Since the method parses by guessing * the type on the first element, malformed note trees can be read as the * wrong type of tree. * * This method is not recursive, it parses the one tree given to it and * returns the bucket. If there are subtrees for note storage, they are * setup as lazy pointers that will be resolved at a later time. * * @param prefix * common hex digits that all notes within this tree share. The * root tree has {@code prefix.length() == 0}, the first-level * subtrees should be {@code prefix.length()==2}, etc. * @param treeId * the tree to read from the repository. * @param reader * reader to access the tree object. * @return bucket to holding the notes of the specified tree. * @throws IOException * {@code treeId} cannot be accessed. */ static InMemoryNoteBucket parse(AbbreviatedObjectId prefix, final ObjectId treeId, final ObjectReader reader) throws IOException { return new NoteParser(prefix, reader, treeId).parse(); } private final int prefixLen; private final int pathPadding; private NonNoteEntry firstNonNote; private NonNoteEntry lastNonNote; private NoteParser(AbbreviatedObjectId prefix, ObjectReader r, ObjectId t) throws IncorrectObjectTypeException, IOException { super(encodeASCII(prefix.name()), r, t); prefixLen = prefix.length(); // Our path buffer has a '/' that we don't want after the prefix. // Drop it by shifting the path down one position. pathPadding = 0 < prefixLen ? 1 : 0; if (0 < pathPadding) System.arraycopy(path, 0, path, pathPadding, prefixLen); } private InMemoryNoteBucket parse() { InMemoryNoteBucket r = parseTree(); r.nonNotes = firstNonNote; return r; } private InMemoryNoteBucket parseTree() { for (; !eof(); next(1)) { if (pathLen == pathPadding + OBJECT_ID_STRING_LENGTH && isHex()) return parseLeafTree(); else if (getNameLength() == 2 && isHex() && isTree()) return parseFanoutTree(); else storeNonNote(); } // If we cannot determine the style used, assume its a leaf. return new LeafBucket(prefixLen); } private LeafBucket parseLeafTree() { final LeafBucket leaf = new LeafBucket(prefixLen); final MutableObjectId idBuf = new MutableObjectId(); for (; !eof(); next(1)) { if (parseObjectId(idBuf)) leaf.parseOneEntry(idBuf, getEntryObjectId()); else storeNonNote(); } return leaf; } private boolean parseObjectId(MutableObjectId id) { if (pathLen == pathPadding + OBJECT_ID_STRING_LENGTH) { try { id.fromString(path, pathPadding); return true; } catch (ArrayIndexOutOfBoundsException notHex) { return false; } } return false; } private FanoutBucket parseFanoutTree() { final FanoutBucket fanout = new FanoutBucket(prefixLen); for (; !eof(); next(1)) { final int cell = parseFanoutCell(); if (0 <= cell) fanout.setBucket(cell, getEntryObjectId()); else storeNonNote(); } return fanout; } private int parseFanoutCell() { if (getNameLength() == 2 && isTree()) { try { return (parseHexInt4(path[pathOffset + 0]) << 4) | parseHexInt4(path[pathOffset + 1]); } catch (ArrayIndexOutOfBoundsException notHex) { return -1; } } return -1; } private void storeNonNote() { ObjectId id = getEntryObjectId(); FileMode fileMode = getEntryFileMode(); byte[] name = new byte[getNameLength()]; getName(name, 0); NonNoteEntry ent = new NonNoteEntry(name, fileMode, id); if (firstNonNote == null) firstNonNote = ent; if (lastNonNote != null) lastNonNote.next = ent; lastNonNote = ent; } private boolean isTree() { return TREE.equals(mode); } private boolean isHex() { try { for (int i = pathOffset; i < pathLen; i++) parseHexInt4(path[i]); return true; } catch (ArrayIndexOutOfBoundsException fail) { return false; } } }