| /* | |
| * Copyright (C) 2008-2010, Google Inc. | |
| * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> 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.treewalk; | |
| import static org.eclipse.jgit.lib.Constants.DOT_GIT_ATTRIBUTES; | |
| import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; | |
| import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; | |
| import static org.eclipse.jgit.lib.Constants.OBJ_TREE; | |
| import static org.eclipse.jgit.lib.Constants.TYPE_TREE; | |
| import static org.eclipse.jgit.lib.Constants.encode; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import org.eclipse.jgit.attributes.AttributesNode; | |
| import org.eclipse.jgit.attributes.AttributesRule; | |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; | |
| import org.eclipse.jgit.errors.MissingObjectException; | |
| import org.eclipse.jgit.lib.AnyObjectId; | |
| import org.eclipse.jgit.lib.FileMode; | |
| import org.eclipse.jgit.lib.MutableObjectId; | |
| import org.eclipse.jgit.lib.ObjectId; | |
| import org.eclipse.jgit.lib.ObjectReader; | |
| /** | |
| * Parses raw Git trees from the canonical semi-text/semi-binary format. | |
| */ | |
| public class CanonicalTreeParser extends AbstractTreeIterator { | |
| private static final byte[] EMPTY = {}; | |
| private static final byte[] ATTRS = encode(DOT_GIT_ATTRIBUTES); | |
| private byte[] raw; | |
| /** First offset within {@link #raw} of the prior entry. */ | |
| private int prevPtr; | |
| /** First offset within {@link #raw} of the current entry's data. */ | |
| private int currPtr; | |
| /** Offset one past the current entry (first byte of next entry). */ | |
| private int nextPtr; | |
| /** | |
| * Create a new parser. | |
| */ | |
| public CanonicalTreeParser() { | |
| reset(EMPTY); | |
| } | |
| /** | |
| * Create a new parser for a tree appearing in a subset of a repository. | |
| * | |
| * @param prefix | |
| * position of this iterator in the repository tree. The value | |
| * may be null or the empty array to indicate the prefix is the | |
| * root of the repository. A trailing slash ('/') is | |
| * automatically appended if the prefix does not end in '/'. | |
| * @param reader | |
| * reader to load the tree data from. | |
| * @param treeId | |
| * identity of the tree being parsed; used only in exception | |
| * messages if data corruption is found. | |
| * @throws MissingObjectException | |
| * the object supplied is not available from the repository. | |
| * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException | |
| * the object supplied as an argument is not actually a tree and | |
| * cannot be parsed as though it were a tree. | |
| * @throws java.io.IOException | |
| * a loose object or pack file could not be read. | |
| */ | |
| public CanonicalTreeParser(final byte[] prefix, final ObjectReader reader, | |
| final AnyObjectId treeId) throws IncorrectObjectTypeException, | |
| IOException { | |
| super(prefix); | |
| reset(reader, treeId); | |
| } | |
| private CanonicalTreeParser(CanonicalTreeParser p) { | |
| super(p); | |
| } | |
| /** | |
| * Get the parent of this tree parser. | |
| * | |
| * @return the parent of this tree parser. | |
| * @deprecated internal use only | |
| */ | |
| @Deprecated | |
| public CanonicalTreeParser getParent() { | |
| return (CanonicalTreeParser) parent; | |
| } | |
| /** | |
| * Reset this parser to walk through the given tree data. | |
| * | |
| * @param treeData | |
| * the raw tree content. | |
| */ | |
| public void reset(byte[] treeData) { | |
| attributesNode = null; | |
| raw = treeData; | |
| prevPtr = -1; | |
| currPtr = 0; | |
| if (eof()) | |
| nextPtr = 0; | |
| else | |
| parseEntry(); | |
| } | |
| /** | |
| * Reset this parser to walk through the given tree. | |
| * | |
| * @param reader | |
| * reader to use during repository access. | |
| * @param id | |
| * identity of the tree being parsed; used only in exception | |
| * messages if data corruption is found. | |
| * @return the root level parser. | |
| * @throws MissingObjectException | |
| * the object supplied is not available from the repository. | |
| * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException | |
| * the object supplied as an argument is not actually a tree and | |
| * cannot be parsed as though it were a tree. | |
| * @throws java.io.IOException | |
| * a loose object or pack file could not be read. | |
| */ | |
| public CanonicalTreeParser resetRoot(final ObjectReader reader, | |
| final AnyObjectId id) throws IncorrectObjectTypeException, | |
| IOException { | |
| CanonicalTreeParser p = this; | |
| while (p.parent != null) | |
| p = (CanonicalTreeParser) p.parent; | |
| p.reset(reader, id); | |
| return p; | |
| } | |
| /** | |
| * Get this iterator, or its parent, if the tree is at eof. | |
| * | |
| * @return this iterator, or its parent, if the tree is at eof. | |
| */ | |
| public CanonicalTreeParser next() { | |
| CanonicalTreeParser p = this; | |
| for (;;) { | |
| if (p.nextPtr == p.raw.length) { | |
| // This parser has reached EOF, return to the parent. | |
| if (p.parent == null) { | |
| p.currPtr = p.nextPtr; | |
| return p; | |
| } | |
| p = (CanonicalTreeParser) p.parent; | |
| continue; | |
| } | |
| p.prevPtr = p.currPtr; | |
| p.currPtr = p.nextPtr; | |
| p.parseEntry(); | |
| return p; | |
| } | |
| } | |
| /** | |
| * Reset this parser to walk through the given tree. | |
| * | |
| * @param reader | |
| * reader to use during repository access. | |
| * @param id | |
| * identity of the tree being parsed; used only in exception | |
| * messages if data corruption is found. | |
| * @throws MissingObjectException | |
| * the object supplied is not available from the repository. | |
| * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException | |
| * the object supplied as an argument is not actually a tree and | |
| * cannot be parsed as though it were a tree. | |
| * @throws java.io.IOException | |
| * a loose object or pack file could not be read. | |
| */ | |
| public void reset(ObjectReader reader, AnyObjectId id) | |
| throws IncorrectObjectTypeException, IOException { | |
| reset(reader.open(id, OBJ_TREE).getCachedBytes()); | |
| } | |
| @Override | |
| public CanonicalTreeParser createSubtreeIterator(final ObjectReader reader, | |
| final MutableObjectId idBuffer) | |
| throws IncorrectObjectTypeException, IOException { | |
| idBuffer.fromRaw(idBuffer(), idOffset()); | |
| if (!FileMode.TREE.equals(mode)) { | |
| final ObjectId me = idBuffer.toObjectId(); | |
| throw new IncorrectObjectTypeException(me, TYPE_TREE); | |
| } | |
| return createSubtreeIterator0(reader, idBuffer); | |
| } | |
| /** | |
| * Back door to quickly create a subtree iterator for any subtree. | |
| * <p> | |
| * Don't use this unless you are ObjectWalk. The method is meant to be | |
| * called only once the current entry has been identified as a tree and its | |
| * identity has been converted into an ObjectId. | |
| * | |
| * @param reader | |
| * reader to load the tree data from. | |
| * @param id | |
| * ObjectId of the tree to open. | |
| * @return a new parser that walks over the current subtree. | |
| * @throws java.io.IOException | |
| * a loose object or pack file could not be read. | |
| */ | |
| public final CanonicalTreeParser createSubtreeIterator0( | |
| final ObjectReader reader, final AnyObjectId id) | |
| throws IOException { | |
| final CanonicalTreeParser p = new CanonicalTreeParser(this); | |
| p.reset(reader, id); | |
| return p; | |
| } | |
| @Override | |
| public CanonicalTreeParser createSubtreeIterator(ObjectReader reader) | |
| throws IncorrectObjectTypeException, IOException { | |
| return createSubtreeIterator(reader, new MutableObjectId()); | |
| } | |
| @Override | |
| public boolean hasId() { | |
| return true; | |
| } | |
| @Override | |
| public byte[] idBuffer() { | |
| return raw; | |
| } | |
| @Override | |
| public int idOffset() { | |
| return nextPtr - OBJECT_ID_LENGTH; | |
| } | |
| @Override | |
| public void reset() { | |
| if (!first()) | |
| reset(raw); | |
| } | |
| @Override | |
| public boolean first() { | |
| return currPtr == 0; | |
| } | |
| @Override | |
| public boolean eof() { | |
| return currPtr == raw.length; | |
| } | |
| @Override | |
| public void next(int delta) { | |
| if (delta == 1) { | |
| // Moving forward one is the most common case. | |
| // | |
| prevPtr = currPtr; | |
| currPtr = nextPtr; | |
| if (!eof()) | |
| parseEntry(); | |
| return; | |
| } | |
| // Fast skip over records, then parse the last one. | |
| // | |
| final int end = raw.length; | |
| int ptr = nextPtr; | |
| while (--delta > 0 && ptr != end) { | |
| prevPtr = ptr; | |
| while (raw[ptr] != 0) | |
| ptr++; | |
| ptr += OBJECT_ID_LENGTH + 1; | |
| } | |
| if (delta != 0) | |
| throw new ArrayIndexOutOfBoundsException(delta); | |
| currPtr = ptr; | |
| if (!eof()) | |
| parseEntry(); | |
| } | |
| @Override | |
| public void back(int delta) { | |
| if (delta == 1 && 0 <= prevPtr) { | |
| // Moving back one is common in NameTreeWalk, as the average tree | |
| // won't have D/F type conflicts to study. | |
| // | |
| currPtr = prevPtr; | |
| prevPtr = -1; | |
| if (!eof()) | |
| parseEntry(); | |
| return; | |
| } else if (delta <= 0) | |
| throw new ArrayIndexOutOfBoundsException(delta); | |
| // Fast skip through the records, from the beginning of the tree. | |
| // There is no reliable way to read the tree backwards, so we must | |
| // parse all over again from the beginning. We hold the last "delta" | |
| // positions in a buffer, so we can find the correct position later. | |
| // | |
| final int[] trace = new int[delta + 1]; | |
| Arrays.fill(trace, -1); | |
| int ptr = 0; | |
| while (ptr != currPtr) { | |
| System.arraycopy(trace, 1, trace, 0, delta); | |
| trace[delta] = ptr; | |
| while (raw[ptr] != 0) | |
| ptr++; | |
| ptr += OBJECT_ID_LENGTH + 1; | |
| } | |
| if (trace[1] == -1) | |
| throw new ArrayIndexOutOfBoundsException(delta); | |
| prevPtr = trace[0]; | |
| currPtr = trace[1]; | |
| parseEntry(); | |
| } | |
| private void parseEntry() { | |
| int ptr = currPtr; | |
| byte c = raw[ptr++]; | |
| int tmp = c - '0'; | |
| for (;;) { | |
| c = raw[ptr++]; | |
| if (' ' == c) | |
| break; | |
| tmp <<= 3; | |
| tmp += c - '0'; | |
| } | |
| mode = tmp; | |
| tmp = pathOffset; | |
| for (;; tmp++) { | |
| c = raw[ptr++]; | |
| if (c == 0) { | |
| break; | |
| } | |
| if (tmp >= path.length) { | |
| growPath(tmp); | |
| } | |
| path[tmp] = c; | |
| } | |
| pathLen = tmp; | |
| nextPtr = ptr + OBJECT_ID_LENGTH; | |
| } | |
| /** | |
| * Retrieve the {@link org.eclipse.jgit.attributes.AttributesNode} for the | |
| * current entry. | |
| * | |
| * @param reader | |
| * {@link org.eclipse.jgit.lib.ObjectReader} used to parse the | |
| * .gitattributes entry. | |
| * @return {@link org.eclipse.jgit.attributes.AttributesNode} for the | |
| * current entry. | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 4.2 | |
| */ | |
| public AttributesNode getEntryAttributesNode(ObjectReader reader) | |
| throws IOException { | |
| if (attributesNode == null) { | |
| attributesNode = findAttributes(reader); | |
| } | |
| return attributesNode.getRules().isEmpty() ? null : attributesNode; | |
| } | |
| private AttributesNode findAttributes(ObjectReader reader) | |
| throws IOException { | |
| CanonicalTreeParser itr = new CanonicalTreeParser(); | |
| itr.reset(raw); | |
| if (itr.findFile(ATTRS)) { | |
| return loadAttributes(reader, itr.getEntryObjectId()); | |
| } | |
| return noAttributes(); | |
| } | |
| private static AttributesNode loadAttributes(ObjectReader reader, | |
| AnyObjectId id) throws IOException { | |
| AttributesNode r = new AttributesNode(); | |
| try (InputStream in = reader.open(id, OBJ_BLOB).openStream()) { | |
| r.parse(in); | |
| } | |
| return r.getRules().isEmpty() ? noAttributes() : r; | |
| } | |
| private static AttributesNode noAttributes() { | |
| return new AttributesNode(Collections.<AttributesRule> emptyList()); | |
| } | |
| } |