// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.actions;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.devtools.build.lib.vfs.PathFragment.pathFragmentCodec;
import static java.nio.charset.StandardCharsets.ISO_8859_1;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.hash.HashFunction;
import com.google.common.io.BaseEncoding;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.pkgcache.PackagePathCodecDependencies;
import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext;
import com.google.devtools.build.lib.skyframe.serialization.ObjectCodec;
import com.google.devtools.build.lib.skyframe.serialization.SerializationContext;
import com.google.devtools.build.lib.skyframe.serialization.SerializationException;
import com.google.devtools.build.lib.skyframe.serialization.VisibleForSerialization;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import com.google.devtools.build.lib.util.Fingerprint;
import com.google.devtools.build.lib.util.HashCodes;
import com.google.devtools.build.lib.vfs.DigestUtils;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.Symlinks;
import com.google.devtools.build.lib.vfs.SyscallCache;
import com.google.devtools.build.lib.vfs.XattrProvider;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.errorprone.annotations.Keep;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.util.Arrays;
import java.util.Objects;
import javax.annotation.Nullable;

/**
 * A value that represents a file for the purposes of up-to-dateness checks of actions.
 *
 * <p>It always stands for an actual file. In particular, tree artifacts and runfiles trees do not
 * have a corresponding {@link FileArtifactValue}. However, the file is not necessarily present in
 * the file system; this happens when intermediate build outputs are not downloaded (and maybe when
 * an input artifact of an action is missing?)
 *
 * <p>It makes its main appearance in {@code ActionExecutionValue.artifactData}. It has two main
 * uses:
 *
 * <ul>
 *   <li>This is how dependent actions get hold of the output metadata of their generated inputs.
 *   <li>This is how {@code FileSystemValueChecker} figures out which actions need to be invalidated
 *       (just propagating the invalidation up from leaf nodes is not enough, because the output
 *       tree may have been changed while Blaze was not looking)
 * </ul>
 *
 * <p>{@link FileArtifactValue} instance equality should only be used for testing purposes. To
 * determine whether a metadata is equivalent to another for invalidation purposes, use {@link
 * #couldBeModifiedSince} or {@link #wasModifiedSinceDigest}.
 */
@Immutable
@ThreadSafe
public abstract class FileArtifactValue implements SkyValue, HasDigest {
  /**
   * The type of the underlying file system object. If it is a regular file, then it is guaranteed
   * to have a digest. Otherwise it does not have a digest.
   */
  public abstract FileStateType getType();

  /**
   * Returns a digest of the content of the underlying file system object; must always return a
   * non-null value for instances of type {@link FileStateType#REGULAR_FILE} that are owned by an
   * {@code ActionExecutionValue}.
   *
   * <p>All instances of this interface must either have a digest or return a last-modified time.
   * Clients should prefer using the digest for content identification (e.g., for caching), and only
   * fall back to the last-modified time if no digest is available.
   *
   * <p>The return value is owned by this object and must not be modified.
   */
  @Override
  public abstract byte[] getDigest();

  /** Returns the file's size, or 0 if the underlying file system object is not a file. */
  // TODO(ulfjack): Throw an exception if it's not a file.
  public abstract long getSize();

  /**
   * Returns the last modified time; see the documentation of {@link #getDigest} for when this can
   * and should be called.
   */
  public abstract long getModifiedTime();

  /**
   * Returns a contents proxy (typically, a subset of the file system object's inode properties)
   * that can be used to detect modifications more cheaply (at the cost of increased chance of a
   * false negative) in situations where a digest would be too expensive to compute.
   *
   * <p>If no proxy is available, returns null.
   */
  @Nullable
  public FileContentsProxy getContentsProxy() {
    return null;
  }

  /**
   * Sets the contents proxy. If this metadata does not support setting the contents proxy, does
   * nothing.
   */
  public void setContentsProxy(FileContentsProxy proxy) {}

  @Nullable
  public byte[] getValueFingerprint() {
    // TODO(janakr): return fingerprint in other cases: symlink, directory.
    return getDigest();
  }

  /**
   * Returns the unresolved symlink target path, which is always normalized.
   *
   * @throws UnsupportedOperationException if the metadata is not of symlink file type.
   */
  public String getUnresolvedSymlinkTarget() {
    throw new UnsupportedOperationException();
  }

  /**
   * Returns whether the file contents are inline, i.e., can be obtained directly from this {@link
   * FileArtifactValue} by calling {@link #getInputStream}.
   */
  public boolean isInline() {
    return false;
  }

  /**
   * Returns an input stream for the inline file contents.
   *
   * @throws UnsupportedOperationException if the file contents are not inline.
   */
  public InputStream getInputStream() {
    throw new UnsupportedOperationException();
  }

  /** Returns whether the file contents exist remotely. */
  public boolean isRemote() {
    return false;
  }

  /** Returns the location index for remote files. For non-remote files, returns 0. */
  public int getLocationIndex() {
    return 0;
  }

  /**
   * Returns the time when the remote file contents may expire. If the contents never expire,
   * including when they're not remote, returns null.
   *
   * <p>The expiration time does not factor into equality, as it can be mutated by {@link
   * #setExpirationTime}.
   */
  @Nullable
  public Instant getExpirationTime() {
    return null;
  }

  /**
   * Sets the expiration time. If this metadata does not support setting the expiration time, does
   * nothing.
   */
  public void setExpirationTime(Instant newExpirationTime) {}

  /**
   * Provides a best-effort determination whether the file was changed since the digest was
   * computed. This method performs file system I/O, so may be expensive. It's primarily intended to
   * avoid storing bad cache entries in an action cache. It should return true if there is a chance
   * that the file was modified since the digest was computed. Better not upload if we are not sure
   * that the cache entry is reliable.
   */
  // TODO(lberki): This is very similar to couldBeModifiedSince(). Check if we can unify these.
  public abstract boolean wasModifiedSinceDigest(Path path) throws IOException;

  /**
   * Returns whether the two {@link FileArtifactValue} instances could be considered the same for
   * purposes of action invalidation.
   */
  // TODO(lberki): This is very similar to wasModifiedSinceDigest(). Check if we can unify these.
  public final boolean couldBeModifiedSince(FileArtifactValue lastKnown) {
    if (this instanceof Singleton || lastKnown instanceof Singleton) {
      return true;
    }

    if (getType() != lastKnown.getType()) {
      return true;
    }

    if (getDigest() != null && lastKnown.getDigest() != null) {
      // If we know the digests, we can tell with certainty whether the file has changed.
      return !Arrays.equals(getDigest(), lastKnown.getDigest()) || getSize() != lastKnown.getSize();
    } else {
      // If not, we assume by default that the file has changed, but individual implementations
      // might know better. For example, regular local files can be compared by ctime or mtime.
      return couldBeModifiedByMetadata(lastKnown);
    }
  }

  /** Adds this file metadata to the given {@link Fingerprint}. */
  public final void addTo(Fingerprint fp) {
    byte[] digest = getDigest();
    if (digest != null) {
      fp.addBytes(digest);
    } else {
      // Use the timestamp if the digest is not present, but not both. Modifying a timestamp while
      // keeping the contents of a file the same should not cause rebuilds.
      fp.addLong(getModifiedTime());
    }
  }

  protected boolean couldBeModifiedByMetadata(FileArtifactValue lastKnown) {
    return true;
  }

  /**
   * Returns the real path at which the file contents this metadata refers to can be found.
   *
   * <p>If present, an artifact possessing this metadata is materialized in the filesystem as a
   * symlink to another artifact, but acts as a copy of that artifact for invalidation purposes.
   * Thus, all other metadata fields reflect the properties of the file system object found at the
   * real path. In particular, this means that {@link #getType} doesn't necessarily return {@link
   * FileStateType#SYMLINK}.
   *
   * <p>The path must be absolute and not contain any unresolved symlinks, i.e., calling {@link
   * Path#resolveSymbolicLinks} on it should yield the same path.
   *
   * <p>This allows such an artifact to be created as a symlink to the real path when lazily
   * materialized on disk, in situations where making a copy is undesirable (e.g. because it would
   * result in redundant downloads of the same remote output file) or impossible (e.g. because the
   * original is a source file or a local output file, and its contents cannot be obtained from the
   * digest). An output service is free to ignore this hint and materialize the artifact in some
   * other way (e.g. as a regular file backed by a FUSE filesystem).
   */
  @Nullable
  public PathFragment getResolvedPath() {
    return null;
  }

  /**
   * Marker interface for singleton implementations of this class.
   *
   * <p>Needed for a correct implementation of {@code equals}.
   */
  interface Singleton {}

  /**
   * Metadata for runfiles trees.
   *
   * <p>This should really be more nuanced so that runfiles trees don't need to be special-cased in
   * the local action cache, but it works well enough. The only downsides are that we don't detect
   * when someone changed a runfiles tree like we do for other output artifacts and a number of
   * extra branches.
   *
   * <p>In Skyframe, we check whether a runfiles tree changed based on {@link
   * RunfilesArtifactValue}, which does contain data about its contents.
   */
  @SerializationConstant
  public static final FileArtifactValue RUNFILES_TREE_MARKER = new SingletonMarkerValue();

  /** Data that marks that a file is not present on the filesystem. */
  @SerializationConstant
  public static final FileArtifactValue MISSING_FILE_MARKER = new SingletonMarkerValue();

  public static FileArtifactValue createForSourceArtifact(
      Artifact artifact, FileValue fileValue, XattrProvider xattrProvider) throws IOException {
    // Artifacts with known generating actions should obtain the derived artifact's SkyValue
    // from the generating action, instead.
    checkState(!artifact.hasKnownGeneratingAction());
    checkState(!artifact.isConstantMetadata());
    boolean isFile = fileValue.isFile();
    return create(
        artifact.getPath(),
        isFile,
        isFile ? fileValue.getSize() : 0,
        isFile ? fileValue.realFileStateValue().getContentsProxy() : null,
        isFile ? fileValue.getDigest() : null,
        xattrProvider);
  }

  public static FileArtifactValue createFromInjectedDigest(
      FileArtifactValue metadata, @Nullable byte[] digest) {
    return createForNormalFile(digest, metadata.getContentsProxy(), metadata.getSize());
  }

  @VisibleForTesting
  public static FileArtifactValue createForTesting(Artifact artifact) throws IOException {
    return createForTesting(artifact.getPath());
  }

  @VisibleForTesting
  public static FileArtifactValue createForTesting(Path path) throws IOException {
    // Caution: there's a race condition between stating the file and computing the digest. We need
    // to stat first, since we're using the stat to detect changes. We follow symlinks here to be
    // consistent with getDigest.
    return createFromStat(path, path.stat(Symlinks.FOLLOW), SyscallCache.NO_CACHE);
  }

  public static FileArtifactValue createFromStat(
      Path path, FileStatus stat, XattrProvider xattrProvider) throws IOException {
    return create(
        path,
        stat.isFile(),
        stat.getSize(),
        FileContentsProxy.create(stat),
        stat instanceof FileStatusWithDigest statWithDigest ? statWithDigest.getDigest() : null,
        xattrProvider);
  }

  private static FileArtifactValue create(
      Path path,
      boolean isFile,
      long size,
      FileContentsProxy proxy,
      @Nullable byte[] digest,
      XattrProvider xattrProvider)
      throws IOException {
    if (!isFile) {
      // In this case, we need to store the mtime because the action cache uses mtime for
      // directories to determine if this artifact has changed. We want this code path to go away
      // somehow.
      return new DirectoryArtifactValue(path.getLastModifiedTime());
    }
    if (digest == null) {
      digest = DigestUtils.getDigestWithManualFallback(path, xattrProvider);
    }
    checkState(digest != null, path);
    return createForNormalFile(digest, proxy, size);
  }

  public static FileArtifactValue createForVirtualActionInput(byte[] digest, long size) {
    return new RegularFileArtifactValue(digest, /* proxy= */ null, size);
  }

  public static FileArtifactValue createForUnresolvedSymlink(Artifact artifact) throws IOException {
    checkArgument(artifact.isSymlink());
    return createForUnresolvedSymlink(artifact.getPath());
  }

  public static FileArtifactValue createForUnresolvedSymlink(Path symlink) throws IOException {
    return new UnresolvedSymlinkArtifactValue(symlink);
  }

  public static FileArtifactValue createForNormalFile(
      byte[] digest, @Nullable FileContentsProxy proxy, long size) {
    return new RegularFileArtifactValue(digest, proxy, size);
  }

  /**
   * Create a FileArtifactValue using the {@link Path} and size. FileArtifactValue#create will
   * handle getting the digest using the Path and size values.
   */
  public static FileArtifactValue createForNormalFileUsingPath(
      Path path, long size, XattrProvider xattrProvider) throws IOException {
    return create(
        path, /* isFile= */ true, size, /* proxy= */ null, /* digest= */ null, xattrProvider);
  }

  public static FileArtifactValue createForDirectoryWithHash(byte[] digest) {
    return new HashedDirectoryArtifactValue(digest);
  }

  public static FileArtifactValue createForDirectoryWithMtime(long mtime) {
    return new DirectoryArtifactValue(mtime);
  }

  public static FileArtifactValue createForInlineFile(byte[] bytes, HashFunction hashFunction) {
    return new InlineFileArtifactValue(bytes, hashFunction.hashBytes(bytes).asBytes());
  }

  /**
   * Prefer {@link #createForRemoteFileWithMaterializationData} if the remote file may be
   * materialized in the local filesystem at a later point as this overload doesn't support {@link
   * #setContentsProxy}.
   */
  public static FileArtifactValue createForRemoteFile(byte[] digest, long size, int locationIndex) {
    return new RemoteFileArtifactValue(digest, size, locationIndex);
  }

  public static FileArtifactValue createForRemoteFileWithMaterializationData(
      byte[] digest, long size, int locationIndex, @Nullable Instant expirationTime) {
    return new RemoteFileArtifactValueWithMaterializationData(
        digest, size, locationIndex, expirationTime);
  }

  public static FileArtifactValue createFromExistingWithResolvedPath(
      FileArtifactValue delegate, PathFragment resolvedPath) {
    return new ResolvedSymlinkArtifactValue(delegate, resolvedPath);
  }

  /**
   * Creates a FileArtifactValue used as a 'proxy' input for a {@link RunfilesArtifactValue}. These
   * are used in {@link ActionCacheChecker}.
   */
  public static FileArtifactValue createRunfilesProxy(byte[] digest) {
    checkNotNull(digest);
    return new RunfilesProxyArtifactValue(digest);
  }

  private static String bytesToString(@Nullable byte[] bytes) {
    return bytes == null ? "null" : "0x" + BaseEncoding.base16().omitPadding().encode(bytes);
  }

  private static final class DirectoryArtifactValue extends FileArtifactValue {
    private final long mtime;

    private DirectoryArtifactValue(long mtime) {
      this.mtime = mtime;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof DirectoryArtifactValue that)) {
        return false;
      }

      return mtime == that.mtime;
    }

    @Override
    public int hashCode() {
      return Long.hashCode(mtime);
    }

    @Override
    public FileStateType getType() {
      return FileStateType.DIRECTORY;
    }

    @Nullable
    @Override
    public byte[] getDigest() {
      return null;
    }

    @Override
    public byte[] getValueFingerprint() {
      return new Fingerprint()
          .addString(getClass().getCanonicalName())
          .addLong(mtime)
          .digestAndReset();
    }

    @Override
    public long getModifiedTime() {
      return mtime;
    }

    @Override
    public long getSize() {
      return 0;
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) {
      return false;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this).add("mtime", mtime).toString();
    }
  }

  private static final class HashedDirectoryArtifactValue extends FileArtifactValue {

    private final byte[] digest;

    private HashedDirectoryArtifactValue(byte[] digest) {
      this.digest = digest;
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof HashedDirectoryArtifactValue that)) {
        return false;
      }

      return Arrays.equals(digest, that.digest);
    }

    @Override
    public int hashCode() {
      return Arrays.hashCode(digest);
    }

    @Override
    public FileStateType getType() {
      return FileStateType.DIRECTORY;
    }

    @Nullable
    @Override
    public byte[] getDigest() {
      return digest;
    }

    @Override
    public long getModifiedTime() {
      return 0;
    }

    @Override
    public long getSize() {
      return 0;
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) {
      // TODO(ulfjack): Ideally, we'd attempt to detect intra-build modifications here. I'm
      // consciously deferring work here as this code will most likely change again, and we're
      // already doing better than before by detecting inter-build modifications.
      return false;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this).add("digest", bytesToString(digest)).toString();
    }
  }

  private static final class RegularFileArtifactValue extends FileArtifactValue {
    private final byte[] digest;
    @Nullable private final FileContentsProxy proxy;
    private final long size;

    private RegularFileArtifactValue(
        @Nullable byte[] digest, @Nullable FileContentsProxy proxy, long size) {
      this.digest = digest;
      this.proxy = proxy;
      this.size = size;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof RegularFileArtifactValue that)) {
        return false;
      }
      return Arrays.equals(digest, that.digest)
          && Objects.equals(proxy, that.proxy)
          && size == that.size;
    }

    @Override
    public int hashCode() {
      return HashCodes.hashObjects(Arrays.hashCode(digest), proxy, size);
    }

    @Override
    public FileStateType getType() {
      return FileStateType.REGULAR_FILE;
    }

    @Override
    public byte[] getDigest() {
      return digest;
    }

    @Override
    public FileContentsProxy getContentsProxy() {
      return proxy;
    }

    @Override
    public long getSize() {
      return size;
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) throws IOException {
      if (proxy == null) {
        return false;
      }
      var stat = path.statIfFound(Symlinks.FOLLOW);
      if (stat == null || !stat.isFile()) {
        // The file no longer exists or changed type, so it certainly has changed.
        return true;
      }
      var newProxy = FileContentsProxy.create(stat);
      if (proxy.equals(newProxy)) {
        // If the proxy is the same, then the file certainly hasn't been modified. This is the
        // common case, so we check it first.
        return false;
      }
      if (proxy.isModified(newProxy)) {
        // If the non-ctime information in the proxy changed, the file has certainly been modified
        // between the time the digest was computed and now.
        return true;
      }
      // At this point the ctime changed, so some of the file's metadata has changed since we
      // computed the digest. Returning true here would allow us to cautiously report modification
      // even in complex ABA scenarios (file modified, then modified back with its mtime reset).
      // However, we would also report modification in case a hardlink to the file was created or
      // removed, such as by the hermetic Linux sandbox or certain optimized copy actions.
      // As a compromise, we check whether the current state of the file differs from the previous
      // one, ignoring any inbetween modifications that may have happened.
      //
      // Note that this path is always taken when using the hermetic Linux sandbox, but the
      // associated cost should amortize over the next build as the digest will be cached under the
      // new stat.
      byte[] newDigest = DigestUtils.getDigestWithManualFallback(path, SyscallCache.NO_CACHE, stat);
      return !Arrays.equals(digest, newDigest);
    }

    @Override
    public long getModifiedTime() {
      throw new UnsupportedOperationException(
          "regular file's mtime should never be called. (" + this + ")");
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("digest", bytesToString(digest))
          .add("size", size)
          .add("proxy", proxy)
          .toString();
    }

    @Override
    protected boolean couldBeModifiedByMetadata(FileArtifactValue lastKnown) {
      return size != lastKnown.getSize() || !Objects.equals(proxy, lastKnown.getContentsProxy());
    }
  }

  /** Proxy metadata for a runfiles tree. */
  private static final class RunfilesProxyArtifactValue extends FileArtifactValue {
    private final byte[] digest;

    private RunfilesProxyArtifactValue(byte[] digest) {
      this.digest = digest;
    }

    @Override
    public FileStateType getType() {
      return FileStateType.DIRECTORY;
    }

    @Override
    public long getSize() {
      return 0;
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) {
      return false;
    }

    @Override
    public byte[] getDigest() {
      return digest;
    }

    @Override
    public long getModifiedTime() {
      throw new UnsupportedOperationException(
          "runfile proxy's mtime should never be called. (" + this + ")");
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof RunfilesProxyArtifactValue that)) {
        return false;
      }
      return Arrays.equals(digest, that.digest);
    }

    @Override
    public int hashCode() {
      return Arrays.hashCode(digest);
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this).add("digest", bytesToString(digest)).toString();
    }
  }

  /** Metadata for remotely stored files. */
  private static class RemoteFileArtifactValue extends FileArtifactValue {
    private final byte[] digest;
    private final long size;
    private final int locationIndex;

    private RemoteFileArtifactValue(byte[] digest, long size, int locationIndex) {
      this.digest = checkNotNull(digest);
      this.size = size;
      this.locationIndex = locationIndex;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof RemoteFileArtifactValue that)) {
        return false;
      }

      return Arrays.equals(digest, that.digest)
          && size == that.size
          && locationIndex == that.locationIndex;
    }

    @Override
    public int hashCode() {
      return HashCodes.hashObjects(Arrays.hashCode(digest), size, locationIndex);
    }

    @Override
    public final FileStateType getType() {
      return FileStateType.REGULAR_FILE;
    }

    @Override
    public final byte[] getDigest() {
      return digest;
    }

    @Override
    public final long getSize() {
      return size;
    }

    @Override
    public final long getModifiedTime() {
      throw new UnsupportedOperationException(
          "RemoteFileArtifactValue doesn't support getModifiedTime");
    }

    @Override
    public final int getLocationIndex() {
      return locationIndex;
    }

    @Override
    public final boolean wasModifiedSinceDigest(Path path) {
      return false;
    }

    @Override
    public final boolean isRemote() {
      return true;
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("digest", bytesToString(digest))
          .add("size", size)
          .add("locationIndex", locationIndex)
          .toString();
    }
  }

  /**
   * Metadata for remotely stored files, with the additional ability to store a {@link
   * #getExpirationTime} modifiable via {@link #setExpirationTime}, and a {@link #getContentsProxy}
   * modifiable via {@link #setContentsProxy}.
   *
   * <p>This is used when the output mode allows for late materialization of remote outputs in the
   * local filesystem.
   */
  private static final class RemoteFileArtifactValueWithMaterializationData
      extends RemoteFileArtifactValue {
    private long expirationTime;
    @Nullable private FileContentsProxy proxy;

    private RemoteFileArtifactValueWithMaterializationData(
        byte[] digest, long size, int locationIndex, @Nullable Instant expirationTime) {
      super(digest, size, locationIndex);
      this.expirationTime = toEpochMilli(expirationTime);
    }

    private static long toEpochMilli(@Nullable Instant expirationTime) {
      return expirationTime != null ? expirationTime.toEpochMilli() : -1;
    }

    @Nullable
    private static Instant fromEpochMilli(long expirationTime) {
      return expirationTime >= 0 ? Instant.ofEpochMilli(expirationTime) : null;
    }

    @Override
    @Nullable
    public Instant getExpirationTime() {
      return fromEpochMilli(expirationTime);
    }

    @Override
    public void setExpirationTime(Instant expirationTime) {
      this.expirationTime = toEpochMilli(expirationTime);
    }

    /**
     * {@inheritDoc}
     *
     * <p>Returns non-null if the file contents have been materialized in the local filesystem.
     */
    @Override
    @Nullable
    public FileContentsProxy getContentsProxy() {
      return proxy;
    }

    /**
     * {@inheritDoc}
     *
     * <p>Called when the file contents are materialized in the local filesystem.
     */
    @Override
    public void setContentsProxy(FileContentsProxy proxy) {
      this.proxy = proxy;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof RemoteFileArtifactValueWithMaterializationData that)) {
        return false;
      }

      return Arrays.equals(getDigest(), that.getDigest())
          && getSize() == that.getSize()
          && getLocationIndex() == that.getLocationIndex();
    }

    @Override
    public int hashCode() {
      return HashCodes.hashObjects(Arrays.hashCode(getDigest()), getSize(), getLocationIndex());
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("digest", bytesToString(getDigest()))
          .add("size", getSize())
          .add("locationIndex", getLocationIndex())
          .add("expirationTime", fromEpochMilli(expirationTime))
          .add("proxy", proxy)
          .toString();
    }
  }

  /**
   * Metadata for an artifact that is materialized in the filesystem as a symlink to another
   * artifact, but acts as a copy of that artifact for invalidation purposes. See the documentation
   * of {@link #getResolvedPath} for when this is useful.
   *
   * <p>Other than {@link #getResolvedPath}, all methods delegate to the {@link FileArtifactValue}
   * of the artifact pointed to, which must itself have a null {@link #getResolvedPath}).
   */
  private static final class ResolvedSymlinkArtifactValue extends FileArtifactValue {
    private final FileArtifactValue delegate;
    private final PathFragment resolvedPath;

    // TODO(b/329460099): Store just the execpath once multiple source roots are no longer
    // supported. At that point it becomes possible to reliably compute the absolute path from the
    // execpath.

    private ResolvedSymlinkArtifactValue(FileArtifactValue delegate, PathFragment resolvedPath) {
      checkArgument(!(delegate instanceof Singleton), "delegate is a singleton: %s", delegate);
      checkArgument(resolvedPath.isAbsolute(), "resolved path is not absolute: %s", resolvedPath);
      checkArgument(
          delegate.getResolvedPath() == null || delegate.getResolvedPath().equals(resolvedPath),
          "delegate has a different resolved path: %s",
          delegate);
      this.delegate =
          delegate instanceof ResolvedSymlinkArtifactValue resolvedDelegate
              ? resolvedDelegate.delegate
              : delegate;
      this.resolvedPath = resolvedPath;
    }

    @Override
    public PathFragment getResolvedPath() {
      return resolvedPath;
    }

    @Override
    public FileStateType getType() {
      return delegate.getType();
    }

    @Nullable
    @Override
    public byte[] getDigest() {
      return delegate.getDigest();
    }

    @Override
    public FileContentsProxy getContentsProxy() {
      return delegate.getContentsProxy();
    }

    @Override
    public void setContentsProxy(FileContentsProxy proxy) {
      delegate.setContentsProxy(proxy);
    }

    @Override
    public long getSize() {
      return delegate.getSize();
    }

    @Override
    public long getModifiedTime() {
      return delegate.getModifiedTime();
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) throws IOException {
      return delegate.wasModifiedSinceDigest(path);
    }

    @Override
    protected boolean couldBeModifiedByMetadata(FileArtifactValue lastKnown) {
      return delegate.couldBeModifiedByMetadata(lastKnown);
    }

    @Override
    public byte[] getValueFingerprint() {
      return delegate.getValueFingerprint();
    }

    @Override
    public boolean isInline() {
      return delegate.isInline();
    }

    @Override
    public InputStream getInputStream() {
      return delegate.getInputStream();
    }

    @Override
    public boolean isRemote() {
      return delegate.isRemote();
    }

    @Override
    public int getLocationIndex() {
      return delegate.getLocationIndex();
    }

    @Override
    @Nullable
    public Instant getExpirationTime() {
      return delegate.getExpirationTime();
    }

    @Override
    public void setExpirationTime(Instant newExpirationTime) {
      delegate.setExpirationTime(newExpirationTime);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ResolvedSymlinkArtifactValue that)) {
        return false;
      }
      return delegate.equals(that.delegate) && resolvedPath.equals(that.resolvedPath);
    }

    @Override
    public int hashCode() {
      return HashCodes.hashObjects(delegate, resolvedPath);
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("delegate", delegate)
          .add("resolvedPath", resolvedPath)
          .toString();
    }
  }

  /**
   * Codec that serializes the absolute {@link ResolvedSymlinkArtifactValue#resolvedPath} by finding
   * its root in {@link PackagePathCodecDependencies} and relativizing.
   */
  // TODO: b/329460099 - This would not be necessary if we could store a source root relative path.
  @Keep // Used reflectively.
  private static final class ResolvedSymlinkArtifactValueCodec
      implements ObjectCodec<ResolvedSymlinkArtifactValue> {

    @Override
    public Class<? extends ResolvedSymlinkArtifactValue> getEncodedClass() {
      return ResolvedSymlinkArtifactValue.class;
    }

    @Override
    public void serialize(
        SerializationContext context, ResolvedSymlinkArtifactValue obj, CodedOutputStream codedOut)
        throws SerializationException, IOException {
      context.serialize(obj.delegate, codedOut);

      PathFragment resolvedPath = obj.resolvedPath;
      ImmutableList<Root> roots =
          context.getDependency(PackagePathCodecDependencies.class).getPackageRoots();
      for (int i = 0; i < roots.size(); i++) {
        Root root = roots.get(i);
        if (root.contains(resolvedPath)) {
          PathFragment relativePath = root.relativize(resolvedPath);
          context.serializeLeaf(relativePath, pathFragmentCodec(), codedOut);
          codedOut.write((byte) i);
          return;
        }
      }
      throw new SerializationException(resolvedPath + " is not under any package roots: " + roots);
    }

    @Override
    public ResolvedSymlinkArtifactValue deserialize(
        DeserializationContext context, CodedInputStream codedIn)
        throws SerializationException, IOException {
      FileArtifactValue delegate = context.deserialize(codedIn);
      PathFragment relativePath = context.deserializeLeaf(codedIn, pathFragmentCodec());
      int rootIndex = codedIn.readRawByte();
      Root root =
          context
              .getDependency(PackagePathCodecDependencies.class)
              .getPackageRoots()
              .get(rootIndex);
      PathFragment resolvedPath = root.getRelative(relativePath).asFragment();
      return new ResolvedSymlinkArtifactValue(delegate, resolvedPath);
    }
  }

  /**
   * Metadata for a symlink that is not to be resolved.
   *
   * <p>Unlike {@link ResolvedSymlinkArtifactValue}, only the textual contents of the symlink matter
   * for invalidation purposes.
   */
  private static final class UnresolvedSymlinkArtifactValue extends FileArtifactValue {
    private final String symlinkTarget;
    private final byte[] digest;

    private UnresolvedSymlinkArtifactValue(Path symlink) throws IOException {
      String symlinkTarget = symlink.readSymbolicLink().getPathString();

      byte[] digest =
          symlink
              .getFileSystem()
              .getDigestFunction()
              .getHashFunction()
              .hashString(symlinkTarget, ISO_8859_1)
              .asBytes();

      // We need to be able to tell the difference between a symlink and a file containing the same
      // text. So we transform the digest a bit. This works because if one wants to craft a file
      // with the same digest as a symlink, one would need to mount a preimage attack on the digest
      // function (this would be different if we tweaked the data before applying the hash function)
      digest[0] = (byte) (digest[0] ^ 0xff);

      this.symlinkTarget = symlinkTarget;
      this.digest = digest;
    }

    @Override
    public String getUnresolvedSymlinkTarget() {
      return symlinkTarget;
    }

    @Override
    public FileStateType getType() {
      return FileStateType.SYMLINK;
    }

    @Override
    public byte[] getDigest() {
      return digest;
    }

    @Override
    public long getSize() {
      return 0;
    }

    @Override
    public long getModifiedTime() {
      throw new IllegalStateException();
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof UnresolvedSymlinkArtifactValue)) {
        return false;
      }
      UnresolvedSymlinkArtifactValue that = (UnresolvedSymlinkArtifactValue) o;
      return Arrays.equals(digest, that.digest);
    }

    @Override
    public int hashCode() {
      return Arrays.hashCode(digest);
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) {
      try {
        var newMetadata = FileArtifactValue.createForUnresolvedSymlink(path);
        return !Arrays.equals(digest, newMetadata.getDigest());
      } catch (IOException e) {
        return true;
      }
    }
  }

  /** Metadata for files whose contents are available in memory. */
  @AutoCodec
  @VisibleForSerialization
  public static final class InlineFileArtifactValue extends FileArtifactValue {
    private final byte[] data;
    private final byte[] digest;

    InlineFileArtifactValue(byte[] data, byte[] digest) {
      this.data = checkNotNull(data);
      this.digest = checkNotNull(digest);
    }

    @Override
    public boolean isInline() {
      return true;
    }

    @Override
    public ByteArrayInputStream getInputStream() {
      return new ByteArrayInputStream(data);
    }

    @Override
    public FileStateType getType() {
      return FileStateType.REGULAR_FILE;
    }

    @Override
    public byte[] getDigest() {
      return digest;
    }

    @Override
    public long getSize() {
      return data.length;
    }

    @Override
    public long getModifiedTime() {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) {
      throw new UnsupportedOperationException();
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof InlineFileArtifactValue that)) {
        return false;
      }
      return Arrays.equals(digest, that.digest);
    }

    @Override
    public int hashCode() {
      return Arrays.hashCode(digest);
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("digest", bytesToString(digest))
          .add("size", getSize())
          .toString();
    }
  }

  /**
   * Metadata for an artifact obtained via a path proxy.
   *
   * <p>This is used to inform action file systems which would otherwise not read local disk that
   * the source of truth for an output is at {@link #getTargetPath}.
   */
  public static final class ProxyFileArtifactValue extends FileArtifactValue {
    private final FileArtifactValue delegate;
    private final Path path;

    public ProxyFileArtifactValue(FileArtifactValue delegate, Path path) {
      this.delegate = checkNotNull(delegate);
      this.path = checkNotNull(path);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ProxyFileArtifactValue that)) {
        return false;
      }
      return this.delegate.equals(that.delegate) && this.path.equals(that.path);
    }

    @Override
    public int hashCode() {
      return HashCodes.hashObjects(delegate, path);
    }

    public Path getTargetPath() {
      return path;
    }

    @Override
    public FileStateType getType() {
      return delegate.getType();
    }

    @Override
    public byte[] getDigest() {
      return delegate.getDigest();
    }

    @Override
    @Nullable
    public FileContentsProxy getContentsProxy() {
      return delegate.getContentsProxy();
    }

    @Override
    public void setContentsProxy(FileContentsProxy proxy) {
      delegate.setContentsProxy(proxy);
    }

    @Override
    public long getSize() {
      return delegate.getSize();
    }

    @Override
    public long getModifiedTime() {
      return delegate.getModifiedTime();
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) throws IOException {
      return delegate.wasModifiedSinceDigest(path);
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("delegate", delegate)
          .add("path", path)
          .toString();
    }
  }

  private static final class SingletonMarkerValue extends FileArtifactValue implements Singleton {
    private static final byte[] FINGERPRINT = new byte[] {0x10};

    @Override
    public FileStateType getType() {
      return FileStateType.NONEXISTENT;
    }

    @Nullable
    @Override
    public byte[] getDigest() {
      return null;
    }

    @Override
    public long getSize() {
      return 0;
    }

    @Override
    public long getModifiedTime() {
      return 0;
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) {
      return false;
    }

    @Override
    public byte[] getValueFingerprint() {
      return FINGERPRINT;
    }

    @Override
    public String toString() {
      return "singleton marker artifact value (" + hashCode() + ")";
    }
  }

  /** {@link FileArtifactValue} subclass for artifacts with constant metadata. A singleton. */
  public static final class ConstantMetadataValue extends FileArtifactValue
      implements FileArtifactValue.Singleton {
    static final ConstantMetadataValue INSTANCE = new ConstantMetadataValue();
    // This needs to not be of length 0, so it is distinguishable from a missing digest when written
    // into a Fingerprint.
    private static final byte[] DIGEST = new byte[1];

    private ConstantMetadataValue() {}

    @Override
    public FileStateType getType() {
      return FileStateType.REGULAR_FILE;
    }

    @Override
    public byte[] getDigest() {
      return DIGEST;
    }

    @Override
    public long getSize() {
      return 0;
    }

    @Override
    public long getModifiedTime() {
      return -1;
    }

    @Override
    public boolean wasModifiedSinceDigest(Path path) {
      throw new UnsupportedOperationException(
          "ConstantMetadataValue doesn't support wasModifiedSinceDigest " + path);
    }
  }
}
