/* * Copyright (C) 2008, 2009, Google Inc. * Copyright (C) 2008, Shawn O. Pearce * Copyright (C) 2010, Matthias Sohn * Copyright (C) 2010, 2020, Christian Halstrick 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.dircache; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.text.MessageFormat; import java.time.Instant; import java.util.Arrays; import org.eclipse.jgit.dircache.DirCache.DirCacheVersion; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.MutableInteger; import org.eclipse.jgit.util.NB; import org.eclipse.jgit.util.SystemReader; /** * A single file (or stage of a file) in a * {@link org.eclipse.jgit.dircache.DirCache}. *

* An entry represents exactly one stage of a file. If a file path is unmerged * then multiple DirCacheEntry instances may appear for the same path name. */ public class DirCacheEntry { private static final byte[] nullpad = new byte[8]; /** The standard (fully merged) stage for an entry. */ public static final int STAGE_0 = 0; /** The base tree revision for an entry. */ public static final int STAGE_1 = 1; /** The first tree revision (usually called "ours"). */ public static final int STAGE_2 = 2; /** The second tree revision (usually called "theirs"). */ public static final int STAGE_3 = 3; private static final int P_CTIME = 0; // private static final int P_CTIME_NSEC = 4; private static final int P_MTIME = 8; // private static final int P_MTIME_NSEC = 12; // private static final int P_DEV = 16; // private static final int P_INO = 20; private static final int P_MODE = 24; // private static final int P_UID = 28; // private static final int P_GID = 32; private static final int P_SIZE = 36; private static final int P_OBJECTID = 40; private static final int P_FLAGS = 60; private static final int P_FLAGS2 = 62; /** Mask applied to data in {@link #P_FLAGS} to get the name length. */ private static final int NAME_MASK = 0xfff; private static final int INTENT_TO_ADD = 0x20000000; private static final int SKIP_WORKTREE = 0x40000000; private static final int EXTENDED_FLAGS = (INTENT_TO_ADD | SKIP_WORKTREE); private static final int INFO_LEN = 62; private static final int INFO_LEN_EXTENDED = 64; private static final int EXTENDED = 0x40; private static final int ASSUME_VALID = 0x80; /** In-core flag signaling that the entry should be considered as modified. */ private static final int UPDATE_NEEDED = 0x1; /** (Possibly shared) header information storage. */ private final byte[] info; /** First location within {@link #info} where our header starts. */ private final int infoOffset; /** Our encoded path name, from the root of the repository. */ final byte[] path; /** Flags which are never stored to disk. */ private byte inCoreFlags; DirCacheEntry(byte[] sharedInfo, MutableInteger infoAt, InputStream in, MessageDigest md, Instant smudge, DirCacheVersion version, DirCacheEntry previous) throws IOException { info = sharedInfo; infoOffset = infoAt.value; IO.readFully(in, info, infoOffset, INFO_LEN); int len; if (isExtended()) { len = INFO_LEN_EXTENDED; IO.readFully(in, info, infoOffset + INFO_LEN, INFO_LEN_EXTENDED - INFO_LEN); if ((getExtendedFlags() & ~EXTENDED_FLAGS) != 0) throw new IOException(MessageFormat.format(JGitText.get() .DIRCUnrecognizedExtendedFlags, String.valueOf(getExtendedFlags()))); } else len = INFO_LEN; infoAt.value += len; md.update(info, infoOffset, len); int toRemove = 0; if (version == DirCacheVersion.DIRC_VERSION_PATHCOMPRESS) { // Read variable int and update digest int b = in.read(); md.update((byte) b); toRemove = b & 0x7F; while ((b & 0x80) != 0) { toRemove++; b = in.read(); md.update((byte) b); toRemove = (toRemove << 7) | (b & 0x7F); } if (toRemove < 0 || (previous != null && toRemove > previous.path.length)) { if (previous == null) { throw new IOException(MessageFormat.format( JGitText.get().DIRCCorruptLengthFirst, Integer.valueOf(toRemove))); } throw new IOException(MessageFormat.format( JGitText.get().DIRCCorruptLength, Integer.valueOf(toRemove), previous.getPathString())); } } int pathLen = NB.decodeUInt16(info, infoOffset + P_FLAGS) & NAME_MASK; int skipped = 0; if (pathLen < NAME_MASK) { path = new byte[pathLen]; if (version == DirCacheVersion.DIRC_VERSION_PATHCOMPRESS && previous != null) { System.arraycopy(previous.path, 0, path, 0, previous.path.length - toRemove); IO.readFully(in, path, previous.path.length - toRemove, pathLen - (previous.path.length - toRemove)); md.update(path, previous.path.length - toRemove, pathLen - (previous.path.length - toRemove)); pathLen = pathLen - (previous.path.length - toRemove); } else { IO.readFully(in, path, 0, pathLen); md.update(path, 0, pathLen); } } else if (version != DirCacheVersion.DIRC_VERSION_PATHCOMPRESS || previous == null || toRemove == previous.path.length) { ByteArrayOutputStream tmp = new ByteArrayOutputStream(); byte[] buf = new byte[NAME_MASK]; IO.readFully(in, buf, 0, NAME_MASK); tmp.write(buf); readNulTerminatedString(in, tmp); path = tmp.toByteArray(); pathLen = path.length; md.update(path, 0, pathLen); skipped = 1; // we already skipped 1 '\0' in readNulTerminatedString md.update((byte) 0); } else { ByteArrayOutputStream tmp = new ByteArrayOutputStream(); tmp.write(previous.path, 0, previous.path.length - toRemove); pathLen = readNulTerminatedString(in, tmp); path = tmp.toByteArray(); md.update(path, previous.path.length - toRemove, pathLen); skipped = 1; // we already skipped 1 '\0' in readNulTerminatedString md.update((byte) 0); } try { checkPath(path); } catch (InvalidPathException e) { CorruptObjectException p = new CorruptObjectException(e.getMessage()); if (e.getCause() != null) p.initCause(e.getCause()); throw p; } if (version == DirCacheVersion.DIRC_VERSION_PATHCOMPRESS) { if (skipped == 0) { int b = in.read(); if (b < 0) { throw new EOFException(JGitText.get().shortReadOfBlock); } md.update((byte) b); } } else { // Index records are padded out to the next 8 byte alignment // for historical reasons related to how C Git read the files. // final int actLen = len + pathLen; final int expLen = (actLen + 8) & ~7; final int padLen = expLen - actLen - skipped; if (padLen > 0) { IO.skipFully(in, padLen); md.update(nullpad, 0, padLen); } } if (mightBeRacilyClean(smudge)) { smudgeRacilyClean(); } } /** * Create an empty entry at stage 0. * * @param newPath * name of the cache entry. * @throws java.lang.IllegalArgumentException * If the path starts or ends with "/", or contains "//" either * "\0". These sequences are not permitted in a git tree object * or DirCache file. */ public DirCacheEntry(String newPath) { this(Constants.encode(newPath), STAGE_0); } /** * Create an empty entry at the specified stage. * * @param newPath * name of the cache entry. * @param stage * the stage index of the new entry. * @throws java.lang.IllegalArgumentException * If the path starts or ends with "/", or contains "//" either * "\0". These sequences are not permitted in a git tree object * or DirCache file. Or if {@code stage} is outside of the * range 0..3, inclusive. */ public DirCacheEntry(String newPath, int stage) { this(Constants.encode(newPath), stage); } /** * Create an empty entry at stage 0. * * @param newPath * name of the cache entry, in the standard encoding. * @throws java.lang.IllegalArgumentException * If the path starts or ends with "/", or contains "//" either * "\0". These sequences are not permitted in a git tree object * or DirCache file. */ public DirCacheEntry(byte[] newPath) { this(newPath, STAGE_0); } /** * Create an empty entry at the specified stage. * * @param path * name of the cache entry, in the standard encoding. * @param stage * the stage index of the new entry. * @throws java.lang.IllegalArgumentException * If the path starts or ends with "/", or contains "//" either * "\0". These sequences are not permitted in a git tree object * or DirCache file. Or if {@code stage} is outside of the * range 0..3, inclusive. */ @SuppressWarnings("boxing") public DirCacheEntry(byte[] path, int stage) { checkPath(path); if (stage < 0 || 3 < stage) throw new IllegalArgumentException(MessageFormat.format( JGitText.get().invalidStageForPath, stage, toString(path))); info = new byte[INFO_LEN]; infoOffset = 0; this.path = path; int flags = ((stage & 0x3) << 12); if (path.length < NAME_MASK) flags |= path.length; else flags |= NAME_MASK; NB.encodeInt16(info, infoOffset + P_FLAGS, flags); } /** * Duplicate DirCacheEntry with same path and copied info. *

* The same path buffer is reused (avoiding copying), however a new info * buffer is created and its contents are copied. * * @param src * entry to clone. * @since 4.2 */ public DirCacheEntry(DirCacheEntry src) { path = src.path; info = new byte[INFO_LEN]; infoOffset = 0; System.arraycopy(src.info, src.infoOffset, info, 0, INFO_LEN); } private int readNulTerminatedString(InputStream in, OutputStream out) throws IOException { int n = 0; for (;;) { int c = in.read(); if (c < 0) { throw new EOFException(JGitText.get().shortReadOfBlock); } if (c == 0) { break; } out.write(c); n++; } return n; } void write(OutputStream os, DirCacheVersion version, DirCacheEntry previous) throws IOException { final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN; if (version != DirCacheVersion.DIRC_VERSION_PATHCOMPRESS) { os.write(info, infoOffset, len); os.write(path, 0, path.length); // Index records are padded out to the next 8 byte alignment // for historical reasons related to how C Git read the files. // int entryLen = len + path.length; int expLen = (entryLen + 8) & ~7; if (entryLen != expLen) os.write(nullpad, 0, expLen - entryLen); } else { int pathCommon = 0; int toRemove; if (previous != null) { // Figure out common prefix int pathLen = Math.min(path.length, previous.path.length); while (pathCommon < pathLen && path[pathCommon] == previous.path[pathCommon]) { pathCommon++; } toRemove = previous.path.length - pathCommon; } else { toRemove = 0; } byte[] tmp = new byte[16]; int n = tmp.length; tmp[--n] = (byte) (toRemove & 0x7F); while ((toRemove >>>= 7) != 0) { tmp[--n] = (byte) (0x80 | (--toRemove & 0x7F)); } os.write(info, infoOffset, len); os.write(tmp, n, tmp.length - n); os.write(path, pathCommon, path.length - pathCommon); os.write(0); } } /** * Is it possible for this entry to be accidentally assumed clean? *

* The "racy git" problem happens when a work file can be updated faster * than the filesystem records file modification timestamps. It is possible * for an application to edit a work file, update the index, then edit it * again before the filesystem will give the work file a new modification * timestamp. This method tests to see if file was written out at the same * time as the index. * * @param smudge_s * seconds component of the index's last modified time. * @param smudge_ns * nanoseconds component of the index's last modified time. * @return true if extra careful checks should be used. * @deprecated use {@link #mightBeRacilyClean(Instant)} instead */ @Deprecated public final boolean mightBeRacilyClean(int smudge_s, int smudge_ns) { return mightBeRacilyClean(Instant.ofEpochSecond(smudge_s, smudge_ns)); } /** * Is it possible for this entry to be accidentally assumed clean? *

* The "racy git" problem happens when a work file can be updated faster * than the filesystem records file modification timestamps. It is possible * for an application to edit a work file, update the index, then edit it * again before the filesystem will give the work file a new modification * timestamp. This method tests to see if file was written out at the same * time as the index. * * @param smudge * index's last modified time. * @return true if extra careful checks should be used. * @since 5.1.9 */ public final boolean mightBeRacilyClean(Instant smudge) { // If the index has a modification time then it came from disk // and was not generated from scratch in memory. In such cases // the entry is 'racily clean' if the entry's cached modification // time is equal to or later than the index modification time. In // such cases the work file is too close to the index to tell if // it is clean or not based on the modification time alone. // final int base = infoOffset + P_MTIME; final int mtime = NB.decodeInt32(info, base); if ((int) smudge.getEpochSecond() == mtime) { return smudge.getNano() <= NB.decodeInt32(info, base + 4); } return false; } /** * Force this entry to no longer match its working tree file. *

* This avoids the "racy git" problem by making this index entry no longer * match the file in the working directory. Later git will be forced to * compare the file content to ensure the file matches the working tree. */ public final void smudgeRacilyClean() { // To mark an entry racily clean we set its length to 0 (like native git // does). Entries which are not racily clean and have zero length can be // distinguished from racily clean entries by checking P_OBJECTID // against the SHA1 of empty content. When length is 0 and P_OBJECTID is // different from SHA1 of empty content we know the entry is marked // racily clean final int base = infoOffset + P_SIZE; Arrays.fill(info, base, base + 4, (byte) 0); } /** * Check whether this entry has been smudged or not *

* If a blob has length 0 we know its id, see * {@link org.eclipse.jgit.lib.Constants#EMPTY_BLOB_ID}. If an entry has * length 0 and an ID different from the one for empty blob we know this * entry was smudged. * * @return true if the entry is smudged, false * otherwise */ public final boolean isSmudged() { final int base = infoOffset + P_OBJECTID; return (getLength() == 0) && (Constants.EMPTY_BLOB_ID.compareTo(info, base) != 0); } final byte[] idBuffer() { return info; } final int idOffset() { return infoOffset + P_OBJECTID; } /** * Is this entry always thought to be unmodified? *

* Most entries in the index do not have this flag set. Users may however * set them on if the file system stat() costs are too high on this working * directory, such as on NFS or SMB volumes. * * @return true if we must assume the entry is unmodified. */ public boolean isAssumeValid() { return (info[infoOffset + P_FLAGS] & ASSUME_VALID) != 0; } /** * Set the assume valid flag for this entry, * * @param assume * true to ignore apparent modifications; false to look at last * modified to detect file modifications. */ public void setAssumeValid(boolean assume) { if (assume) info[infoOffset + P_FLAGS] |= (byte) ASSUME_VALID; else info[infoOffset + P_FLAGS] &= (byte) ~ASSUME_VALID; } /** * Whether this entry should be checked for changes * * @return {@code true} if this entry should be checked for changes */ public boolean isUpdateNeeded() { return (inCoreFlags & UPDATE_NEEDED) != 0; } /** * Set whether this entry must be checked for changes * * @param updateNeeded * whether this entry must be checked for changes */ public void setUpdateNeeded(boolean updateNeeded) { if (updateNeeded) inCoreFlags |= (byte) UPDATE_NEEDED; else inCoreFlags &= (byte) ~UPDATE_NEEDED; } /** * Get the stage of this entry. *

* Entries have one of 4 possible stages: 0-3. * * @return the stage of this entry. */ public int getStage() { return (info[infoOffset + P_FLAGS] >>> 4) & 0x3; } /** * Sets the stage of an entry. * * @param stage * to set, in the range [0..3] * @throws IllegalArgumentException * if the stage is outside the range [0..3] * @since 5.10 */ public void setStage(int stage) { if ((stage & ~0x3) != 0) { throw new IllegalArgumentException( "Invalid stage, must be in range [0..3]"); //$NON-NLS-1$ } byte flags = info[infoOffset + P_FLAGS]; info[infoOffset + P_FLAGS] = (byte) ((flags & 0xCF) | (stage << 4)); } /** * Returns whether this entry should be skipped from the working tree. * * @return true if this entry should be skipepd. */ public boolean isSkipWorkTree() { return (getExtendedFlags() & SKIP_WORKTREE) != 0; } /** * Returns whether this entry is intent to be added to the Index. * * @return true if this entry is intent to add. */ public boolean isIntentToAdd() { return (getExtendedFlags() & INTENT_TO_ADD) != 0; } /** * Returns whether this entry is in the fully-merged stage (0). * * @return true if this entry is merged * @since 2.2 */ public boolean isMerged() { return getStage() == STAGE_0; } /** * Obtain the raw {@link org.eclipse.jgit.lib.FileMode} bits for this entry. * * @return mode bits for the entry. * @see FileMode#fromBits(int) */ public int getRawMode() { return NB.decodeInt32(info, infoOffset + P_MODE); } /** * Obtain the {@link org.eclipse.jgit.lib.FileMode} for this entry. * * @return the file mode singleton for this entry. */ public FileMode getFileMode() { return FileMode.fromBits(getRawMode()); } /** * Set the file mode for this entry. * * @param mode * the new mode constant. * @throws java.lang.IllegalArgumentException * If {@code mode} is * {@link org.eclipse.jgit.lib.FileMode#MISSING}, * {@link org.eclipse.jgit.lib.FileMode#TREE}, or any other type * code not permitted in a tree object. */ public void setFileMode(FileMode mode) { switch (mode.getBits() & FileMode.TYPE_MASK) { case FileMode.TYPE_MISSING: case FileMode.TYPE_TREE: throw new IllegalArgumentException(MessageFormat.format( JGitText.get().invalidModeForPath, mode, getPathString())); } NB.encodeInt32(info, infoOffset + P_MODE, mode.getBits()); } void setFileMode(int mode) { NB.encodeInt32(info, infoOffset + P_MODE, mode); } /** * Get the cached creation time of this file, in milliseconds. * * @return cached creation time of this file, in milliseconds since the * Java epoch (midnight Jan 1, 1970 UTC). */ public long getCreationTime() { return decodeTS(P_CTIME); } /** * Set the cached creation time of this file, using milliseconds. * * @param when * new cached creation time of the file, in milliseconds. */ public void setCreationTime(long when) { encodeTS(P_CTIME, when); } /** * Get the cached last modification date of this file, in milliseconds. *

* One of the indicators that the file has been modified by an application * changing the working tree is if the last modification time for the file * differs from the time stored in this entry. * * @return last modification time of this file, in milliseconds since the * Java epoch (midnight Jan 1, 1970 UTC). * @deprecated use {@link #getLastModifiedInstant()} instead */ @Deprecated public long getLastModified() { return decodeTS(P_MTIME); } /** * Get the cached last modification date of this file. *

* One of the indicators that the file has been modified by an application * changing the working tree is if the last modification time for the file * differs from the time stored in this entry. * * @return last modification time of this file. * @since 5.1.9 */ public Instant getLastModifiedInstant() { return decodeTSInstant(P_MTIME); } /** * Set the cached last modification date of this file, using milliseconds. * * @param when * new cached modification date of the file, in milliseconds. * @deprecated use {@link #setLastModified(Instant)} instead */ @Deprecated public void setLastModified(long when) { encodeTS(P_MTIME, when); } /** * Set the cached last modification date of this file. * * @param when * new cached modification date of the file. * @since 5.1.9 */ public void setLastModified(Instant when) { encodeTS(P_MTIME, when); } /** * Get the cached size (mod 4 GB) (in bytes) of this file. *

* One of the indicators that the file has been modified by an application * changing the working tree is if the size of the file (in bytes) differs * from the size stored in this entry. *

* Note that this is the length of the file in the working directory, which * may differ from the size of the decompressed blob if work tree filters * are being used, such as LF<->CRLF conversion. *

* Note also that for very large files, this is the size of the on-disk file * truncated to 32 bits, i.e. modulo 4294967296. If that value is larger * than 2GB, it will appear negative. * * @return cached size of the working directory file, in bytes. */ public int getLength() { return NB.decodeInt32(info, infoOffset + P_SIZE); } /** * Set the cached size (in bytes) of this file. * * @param sz * new cached size of the file, as bytes. If the file is larger * than 2G, cast it to (int) before calling this method. */ public void setLength(int sz) { NB.encodeInt32(info, infoOffset + P_SIZE, sz); } /** * Set the cached size (in bytes) of this file. * * @param sz * new cached size of the file, as bytes. */ public void setLength(long sz) { setLength((int) sz); } /** * Obtain the ObjectId for the entry. *

* Using this method to compare ObjectId values between entries is * inefficient as it causes memory allocation. * * @return object identifier for the entry. */ public ObjectId getObjectId() { return ObjectId.fromRaw(idBuffer(), idOffset()); } /** * Set the ObjectId for the entry. * * @param id * new object identifier for the entry. May be * {@link org.eclipse.jgit.lib.ObjectId#zeroId()} to remove the * current identifier. */ public void setObjectId(AnyObjectId id) { id.copyRawTo(idBuffer(), idOffset()); } /** * Set the ObjectId for the entry from the raw binary representation. * * @param bs * the raw byte buffer to read from. At least 20 bytes after p * must be available within this byte array. * @param p * position to read the first byte of data from. */ public void setObjectIdFromRaw(byte[] bs, int p) { final int n = Constants.OBJECT_ID_LENGTH; System.arraycopy(bs, p, idBuffer(), idOffset(), n); } /** * Get the entry's complete path. *

* This method is not very efficient and is primarily meant for debugging * and final output generation. Applications should try to avoid calling it, * and if invoked do so only once per interesting entry, where the name is * absolutely required for correct function. * * @return complete path of the entry, from the root of the repository. If * the entry is in a subtree there will be at least one '/' in the * returned string. */ public String getPathString() { return toString(path); } /** * Get a copy of the entry's raw path bytes. * * @return raw path bytes. * @since 3.4 */ public byte[] getRawPath() { return path.clone(); } /** * {@inheritDoc} *

* Use for debugging only ! */ @SuppressWarnings("nls") @Override public String toString() { return getFileMode() + " " + getLength() + " " + getLastModifiedInstant() + " " + getObjectId() + " " + getStage() + " " + getPathString() + "\n"; } /** * Copy the ObjectId and other meta fields from an existing entry. *

* This method copies everything except the path from one entry to another, * supporting renaming. * * @param src * the entry to copy ObjectId and meta fields from. */ public void copyMetaData(DirCacheEntry src) { copyMetaData(src, false); } /** * Copy the ObjectId and other meta fields from an existing entry. *

* This method copies everything except the path and possibly stage from one * entry to another, supporting renaming. * * @param src * the entry to copy ObjectId and meta fields from. * @param keepStage * if true, the stage attribute will not be copied */ void copyMetaData(DirCacheEntry src, boolean keepStage) { int origflags = NB.decodeUInt16(info, infoOffset + P_FLAGS); int newflags = NB.decodeUInt16(src.info, src.infoOffset + P_FLAGS); System.arraycopy(src.info, src.infoOffset, info, infoOffset, INFO_LEN); final int pLen = origflags & NAME_MASK; final int SHIFTED_STAGE_MASK = 0x3 << 12; final int pStageShifted; if (keepStage) pStageShifted = origflags & SHIFTED_STAGE_MASK; else pStageShifted = newflags & SHIFTED_STAGE_MASK; NB.encodeInt16(info, infoOffset + P_FLAGS, pStageShifted | pLen | (newflags & ~NAME_MASK & ~SHIFTED_STAGE_MASK)); } /** * @return true if the entry contains extended flags. */ boolean isExtended() { return (info[infoOffset + P_FLAGS] & EXTENDED) != 0; } private long decodeTS(int pIdx) { final int base = infoOffset + pIdx; final int sec = NB.decodeInt32(info, base); final int ms = NB.decodeInt32(info, base + 4) / 1000000; return 1000L * sec + ms; } private Instant decodeTSInstant(int pIdx) { final int base = infoOffset + pIdx; final int sec = NB.decodeInt32(info, base); final int nano = NB.decodeInt32(info, base + 4); return Instant.ofEpochSecond(sec, nano); } private void encodeTS(int pIdx, long when) { final int base = infoOffset + pIdx; NB.encodeInt32(info, base, (int) (when / 1000)); NB.encodeInt32(info, base + 4, ((int) (when % 1000)) * 1000000); } private void encodeTS(int pIdx, Instant when) { final int base = infoOffset + pIdx; NB.encodeInt32(info, base, (int) when.getEpochSecond()); NB.encodeInt32(info, base + 4, when.getNano()); } private int getExtendedFlags() { if (isExtended()) { return NB.decodeUInt16(info, infoOffset + P_FLAGS2) << 16; } return 0; } private static void checkPath(byte[] path) { try { SystemReader.getInstance().checkPath(path); } catch (CorruptObjectException e) { InvalidPathException p = new InvalidPathException(toString(path)); p.initCause(e); throw p; } } static String toString(byte[] path) { return UTF_8.decode(ByteBuffer.wrap(path)).toString(); } static int getMaximumInfoLength(boolean extended) { return extended ? INFO_LEN_EXTENDED : INFO_LEN; } }