download_and_extract: Show valid prefix suggestions in case of an invalid prefix.

This commit also generalizes exception handling for IOException from
every decompressor's `decompress` to the general `decompress`. Also, it
has introduced a new `IOException`: `CouldNotFindPrefixException` that
handles the suggestions and prints them to the user.

Fixes #7321,
Closes #7333.

PiperOrigin-RevId: 234103706
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/CompressedTarFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/CompressedTarFunction.java
index b3141c7..9a8bde0 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/CompressedTarFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/CompressedTarFunction.java
@@ -16,20 +16,18 @@
 
 import com.google.common.base.Optional;
 import com.google.devtools.build.lib.bazel.repository.DecompressorValue.Decompressor;
-import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
-import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
-
-import java.util.Date;
-import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
-import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
 
 /**
  * Common code for unarchiving a compressed TAR file.
@@ -39,9 +37,10 @@
       throws IOException;
 
   @Override
-  public Path decompress(DecompressorDescriptor descriptor) throws RepositoryFunctionException {
+  public Path decompress(DecompressorDescriptor descriptor) throws IOException {
     Optional<String> prefix = descriptor.prefix();
     boolean foundPrefix = false;
+    Set<String> availablePrefixes = new HashSet<>();
 
     try (InputStream decompressorStream = getDecompressorStream(descriptor)) {
       TarArchiveInputStream tarStream = new TarArchiveInputStream(decompressorStream);
@@ -49,6 +48,15 @@
       while ((entry = tarStream.getNextTarEntry()) != null) {
         StripPrefixedPath entryPath = StripPrefixedPath.maybeDeprefix(entry.getName(), prefix);
         foundPrefix = foundPrefix || entryPath.foundPrefix();
+
+        if (prefix.isPresent() && !foundPrefix) {
+          Optional<String> suggestion =
+              CouldNotFindPrefixException.maybeMakePrefixSuggestion(entryPath.getPathFragment());
+          if (suggestion.isPresent()) {
+            availablePrefixes.add(suggestion.get());
+          }
+        }
+
         if (entryPath.skip()) {
           continue;
         }
@@ -90,14 +98,10 @@
           }
         }
       }
-    } catch (IOException e) {
-      throw new RepositoryFunctionException(e, Transience.TRANSIENT);
-    }
 
-    if (prefix.isPresent() && !foundPrefix) {
-      throw new RepositoryFunctionException(
-          new IOException("Prefix " + prefix.get() + " was given, but not found in the archive"),
-          Transience.PERSISTENT);
+      if (prefix.isPresent() && !foundPrefix) {
+        throw new CouldNotFindPrefixException(prefix.get(), availablePrefixes);
+      }
     }
 
     return descriptor.repositoryPath();
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java
index 0065eef..de0bea3 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/DecompressorValue.java
@@ -14,19 +14,64 @@
 
 package com.google.devtools.build.lib.bazel.repository;
 
+import com.google.common.base.Optional;
 import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
 import com.google.devtools.build.skyframe.SkyValue;
+import java.io.IOException;
+import java.util.Set;
 
 /**
  * The contents of decompressed archive.
  */
 public class DecompressorValue implements SkyValue {
+
   /** Implementation of a decompression algorithm. */
   public interface Decompressor {
-    Path decompress(DecompressorDescriptor descriptor) throws RepositoryFunctionException;
+
+    /** Exception reporting about absence of an expected prefix in an archive. */
+    class CouldNotFindPrefixException extends IOException {
+
+      CouldNotFindPrefixException(String prefix, Set<String> availablePrefixes) {
+
+        super(CouldNotFindPrefixException.prepareErrorMessage(prefix, availablePrefixes));
+      }
+
+      private static String prepareErrorMessage(String prefix, Set<String> availablePrefixes) {
+        String error = "Prefix \"" + prefix + "\" was given, but not found in the archive. ";
+        String suggestion = "Here are possible prefixes for this archive: ";
+        String suggestionBody = "";
+
+        if (availablePrefixes.isEmpty()) {
+          suggestion =
+              "We could not find any directory in this archive"
+                  + " (maybe there is no need for `strip_prefix`?)";
+        } else {
+          // Add a list of possible suggestion wrapped with `"` and separated by `, `.
+          suggestionBody = "\"" + String.join("\", \"", availablePrefixes) + "\".";
+        }
+
+        return error + suggestion + suggestionBody;
+      }
+
+      private static boolean isValidPrefixSuggestion(PathFragment pathFragment) {
+        return pathFragment.segmentCount() > 1;
+      }
+
+      public static Optional<String> maybeMakePrefixSuggestion(PathFragment pathFragment) {
+        if (isValidPrefixSuggestion(pathFragment)) {
+          return Optional.of(pathFragment.getSegment(0));
+        } else {
+          return Optional.absent();
+        }
+      }
+    }
+
+    Path decompress(DecompressorDescriptor descriptor)
+        throws IOException, RepositoryFunctionException;
   }
 
   private final Path directory;
@@ -76,7 +121,17 @@
   }
 
   public static Path decompress(DecompressorDescriptor descriptor)
-      throws RepositoryFunctionException, InterruptedException {
-    return descriptor.getDecompressor().decompress(descriptor);
+      throws RepositoryFunctionException {
+    try {
+      return descriptor.getDecompressor().decompress(descriptor);
+    } catch (IOException e) {
+      Path destinationDirectory = descriptor.archivePath().getParentDirectory();
+      throw new RepositoryFunctionException(
+          new IOException(
+              String.format(
+                  "Error extracting %s to %s: %s",
+                  descriptor.archivePath(), destinationDirectory, e.getMessage())),
+          Transience.TRANSIENT);
+    }
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java
index 7af6b06..c023904 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/ZipDecompressor.java
@@ -18,11 +18,9 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.devtools.build.lib.bazel.repository.DecompressorValue.Decompressor;
-import com.google.devtools.build.lib.rules.repository.RepositoryFunction.RepositoryFunctionException;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
-import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
 import com.google.devtools.build.zip.ZipFileEntry;
 import com.google.devtools.build.zip.ZipReader;
 import java.io.File;
@@ -33,8 +31,9 @@
 import java.nio.file.StandardCopyOption;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
-
+import java.util.Set;
 import javax.annotation.Nullable;
 
 /**
@@ -73,7 +72,7 @@
    */
   @Override
   @Nullable
-  public Path decompress(DecompressorDescriptor descriptor) throws RepositoryFunctionException {
+  public Path decompress(DecompressorDescriptor descriptor) throws IOException {
     Path destinationDirectory = descriptor.archivePath().getParentDirectory();
     Optional<String> prefix = descriptor.prefix();
     boolean foundPrefix = false;
@@ -92,17 +91,20 @@
       for (Map.Entry<Path, PathFragment> symlink : symlinks.entrySet()) {
         symlink.getKey().createSymbolicLink(symlink.getValue());
       }
-    } catch (IOException e) {
-      throw new RepositoryFunctionException(new IOException(
-          String.format("Error extracting %s to %s: %s",
-              descriptor.archivePath(), destinationDirectory, e.getMessage())),
-          Transience.TRANSIENT);
-    }
 
-    if (prefix.isPresent() && !foundPrefix) {
-      throw new RepositoryFunctionException(
-          new IOException("Prefix " + prefix.get() + " was given, but not found in the zip"),
-          Transience.PERSISTENT);
+      if (prefix.isPresent() && !foundPrefix) {
+        Set<String> prefixes = new HashSet<>();
+        for (ZipFileEntry entry : entries) {
+          StripPrefixedPath entryPath =
+              StripPrefixedPath.maybeDeprefix(entry.getName(), Optional.absent());
+          Optional<String> suggestion =
+              CouldNotFindPrefixException.maybeMakePrefixSuggestion(entryPath.getPathFragment());
+          if (suggestion.isPresent()) {
+            prefixes.add(suggestion.get());
+          }
+        }
+        throw new CouldNotFindPrefixException(prefix.get(), prefixes);
+      }
     }
 
     return destinationDirectory;