Skip to content

Commit fa0051d

Browse files
committed
Add support for decompressing .7z files
This implementation follows the implementation of ZipDecompressor with the exception that .7z files do not handle symbolic links or preserving file permissions. The commit 9c98120 where .ar/.deb support was added, was used as a reference for what additional files to change. Closes #27231
1 parent e66fe55 commit fa0051d

File tree

9 files changed

+228
-6
lines changed

9 files changed

+228
-6
lines changed

src/main/java/com/google/devtools/build/lib/bazel/repository/decompressor/DecompressorValue.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,13 @@ static Decompressor getDecompressor(Path archivePath) throws RepositoryFunctionE
112112
return TarBz2Function.INSTANCE;
113113
} else if (baseName.endsWith(".ar") || baseName.endsWith(".deb")) {
114114
return ArFunction.INSTANCE;
115+
} else if (baseName.endsWith(".7z")) {
116+
return SevenZDecompressor.INSTANCE;
115117
} else {
116118
throw new RepositoryFunctionException(
117119
Starlark.errorf(
118120
"Expected a file with a .zip, .jar, .war, .aar, .nupkg, .whl, .tar, .tar.gz, .tgz,"
119-
+ " .tar.xz, , .tar.zst, .tzst, .tar.bz2, .tbz, .ar or .deb suffix (got %s)",
121+
+ " .tar.xz, , .tar.zst, .tzst, .tar.bz2, .tbz, .ar, .deb or .7z suffix (got %s)",
120122
archivePath),
121123
Transience.PERSISTENT);
122124
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.google.devtools.build.lib.bazel.repository.decompressor;
2+
3+
import static java.nio.charset.StandardCharsets.UTF_8;
4+
5+
import com.google.common.io.ByteStreams;
6+
import com.google.devtools.build.lib.bazel.repository.RepositoryFunctionException;
7+
import com.google.devtools.build.lib.bazel.repository.decompressor.DecompressorValue.Decompressor;
8+
import com.google.devtools.build.lib.vfs.Path;
9+
import com.google.devtools.build.lib.vfs.PathFragment;
10+
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
11+
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
12+
13+
import javax.annotation.Nullable;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.io.OutputStream;
17+
import java.util.HashSet;
18+
import java.util.Map;
19+
import java.util.Optional;
20+
import java.util.Set;
21+
22+
/**
23+
* Creates a repository by decompressing a 7-zip file. This implementation generally follows the
24+
* logic from {@link ZipDecompressor} with the exception that the 7z format does not support file
25+
* permissions or symbolic links.
26+
*/
27+
public class SevenZDecompressor implements Decompressor {
28+
public static final Decompressor INSTANCE = new SevenZDecompressor();
29+
30+
/** Decompresses the file to directory {@link DecompressorDescriptor#destinationPath()} */
31+
@Override
32+
@Nullable
33+
public Path decompress(DecompressorDescriptor descriptor)
34+
throws IOException, RepositoryFunctionException, InterruptedException {
35+
Path destinationDirectory = descriptor.destinationPath();
36+
Optional<String> prefix = descriptor.prefix();
37+
Map<String, String> renameFiles = descriptor.renameFiles();
38+
boolean foundPrefix = false;
39+
40+
try (SevenZFile sevenZFile =
41+
SevenZFile.builder().setFile(descriptor.archivePath().getPathFile()).get()) {
42+
Iterable<SevenZArchiveEntry> entries = sevenZFile.getEntries();
43+
for (SevenZArchiveEntry entry : entries) {
44+
String entryName = entry.getName();
45+
entryName = renameFiles.getOrDefault(entryName, entryName);
46+
StripPrefixedPath entryPath =
47+
StripPrefixedPath.maybeDeprefix(entryName.getBytes(UTF_8), prefix);
48+
foundPrefix = foundPrefix || entryPath.foundPrefix();
49+
if (entryPath.skip()) {
50+
continue;
51+
}
52+
extract7zEntry(sevenZFile, entry, destinationDirectory, entryPath.getPathFragment());
53+
}
54+
55+
if (prefix.isPresent() && !foundPrefix) {
56+
Set<String> prefixes = new HashSet<>();
57+
for (SevenZArchiveEntry entry : entries) {
58+
StripPrefixedPath entryPath =
59+
StripPrefixedPath.maybeDeprefix(entry.getName().getBytes(UTF_8), Optional.empty());
60+
CouldNotFindPrefixException.maybeMakePrefixSuggestion(entryPath.getPathFragment())
61+
.ifPresent(prefixes::add);
62+
}
63+
throw new CouldNotFindPrefixException(prefix.get(), prefixes);
64+
}
65+
}
66+
return destinationDirectory;
67+
}
68+
69+
private static void extract7zEntry(
70+
SevenZFile sevenZFile,
71+
SevenZArchiveEntry entry,
72+
Path destinationDirectory,
73+
PathFragment strippedRelativePath)
74+
throws IOException, InterruptedException {
75+
if (strippedRelativePath.isAbsolute()) {
76+
throw new IOException(
77+
String.format(
78+
"Failed to extract %s, 7-zipped paths cannot be absolute", strippedRelativePath));
79+
}
80+
// Sanity/security check - at this point, uplevel references (..) should be resolved.
81+
// There shouldn't be any remaining uplevel references, otherwise, the extracted file could
82+
// "escape" the destination directory.
83+
if (strippedRelativePath.containsUplevelReferences()) {
84+
throw new IOException(
85+
String.format(
86+
"Failed to extract %s, 7-zipped entry contains uplevel references (..)",
87+
strippedRelativePath));
88+
}
89+
Path outputPath = destinationDirectory.getRelative(strippedRelativePath);
90+
outputPath.getParentDirectory().createDirectoryAndParents();
91+
boolean isDirectory = entry.isDirectory();
92+
if (isDirectory) {
93+
outputPath.createDirectoryAndParents();
94+
} else {
95+
try (InputStream input = sevenZFile.getInputStream(entry);
96+
OutputStream output = outputPath.getOutputStream()) {
97+
ByteStreams.copy(input, output);
98+
if (Thread.interrupted()) {
99+
throw new InterruptedException();
100+
}
101+
}
102+
outputPath.setLastModifiedTime(entry.getLastModifiedTime().toMillis());
103+
}
104+
}
105+
}

src/main/java/com/google/devtools/build/lib/bazel/repository/starlark/StarlarkBaseExternalContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ public Object download(
906906
determined from the file extension of the URL. If the file has no \
907907
extension, you can explicitly specify either "zip", "jar", "war", \
908908
"aar", "nupkg", "whl", "tar", "tar.gz", "tgz", "tar.xz", "txz", ".tar.zst", \
909-
".tzst", "tar.bz2", ".tbz", ".ar", or ".deb" here.
909+
".tzst", "tar.bz2", ".tbz", ".ar", ".deb", or ".7z" here.
910910
"""),
911911
@Param(
912912
name = "strip_prefix",

src/test/java/com/google/devtools/build/lib/bazel/repository/decompressor/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ java_library(
1717
name = "DecompressorTests_lib",
1818
srcs = glob(["*.java"]),
1919
data = [
20+
"test_decompress_archive.7z",
2021
"test_decompress_archive.tar.gz",
2122
"test_decompress_archive.zip",
2223
"test_files.ar",
@@ -27,6 +28,7 @@ java_library(
2728
"//src/main/java/com/google/devtools/build/lib/clock",
2829
"//src/main/java/com/google/devtools/build/lib/unix",
2930
"//src/main/java/com/google/devtools/build/lib/util:os",
31+
"//src/main/java/com/google/devtools/build/lib/util:string_encoding",
3032
"//src/main/java/com/google/devtools/build/lib/vfs",
3133
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
3234
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",

src/test/java/com/google/devtools/build/lib/bazel/repository/decompressor/DecompressorValueTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public void testKnownFileExtensionsDoNotThrow() throws Exception {
6767
unused = DecompressorValue.getDecompressor(path);
6868
path = fs.getPath("/foo/.external-repositories/some-repo/bar.baz.deb");
6969
unused = DecompressorValue.getDecompressor(path);
70+
path = fs.getPath("/foo/.external-repositories/some-repo/bar.baz.7z");
71+
unused = DecompressorValue.getDecompressor(path);
7072
}
7173

7274
@Test
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.google.devtools.build.lib.bazel.repository.decompressor;
2+
3+
import com.google.devtools.build.lib.util.StringEncoding;
4+
import com.google.devtools.build.lib.vfs.Path;
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
import org.junit.runner.RunWith;
8+
import org.junit.runners.JUnit4;
9+
10+
import java.util.HashMap;
11+
12+
import static com.google.common.truth.Truth.assertThat;
13+
import static com.google.devtools.build.lib.bazel.repository.decompressor.TestArchiveDescriptor.INNER_FOLDER_NAME;
14+
import static com.google.devtools.build.lib.bazel.repository.decompressor.TestArchiveDescriptor.ROOT_FOLDER_NAME;
15+
16+
/** Tests .7z decompression. */
17+
@RunWith(JUnit4.class)
18+
public class SevenZDecompressorTest {
19+
/**
20+
* .7z file, created with two files:
21+
*
22+
* <ul>
23+
* <li>root_folder/another_folder/regular_file
24+
* <li>root_folder/another_folder/ünïcödëFïlë.txt
25+
* </ul>
26+
*
27+
* Compressed with command "7zz a test_decompress_archive.7z root_folder"
28+
*/
29+
private static final String ARCHIVE_NAME = "test_decompress_archive.7z";
30+
31+
private static final String UNICODE_FILENAME = "ünïcödëFïlë.txt";
32+
33+
private TestArchiveDescriptor archiveDescriptor;
34+
35+
@Before
36+
public void setUpFs() throws Exception {
37+
archiveDescriptor =
38+
new TestArchiveDescriptor(
39+
ARCHIVE_NAME, /* outDirName= */ "out", /* withHardLinks= */ false);
40+
}
41+
42+
/** Test decompressing a .7z file without stripping a prefix */
43+
@Test
44+
public void testDecompressWithoutPrefix() throws Exception {
45+
Path outputDir = decompress(archiveDescriptor.createDescriptorBuilder().build());
46+
47+
archiveDescriptor.assertOutputFiles(outputDir, ROOT_FOLDER_NAME, INNER_FOLDER_NAME);
48+
}
49+
50+
/** Test decompressing a .7z file and stripping a prefix. */
51+
@Test
52+
public void testDecompressWithPrefix() throws Exception {
53+
DecompressorDescriptor.Builder descriptorBuilder =
54+
archiveDescriptor.createDescriptorBuilder().setPrefix(ROOT_FOLDER_NAME);
55+
Path outputDir = decompress(descriptorBuilder.build());
56+
57+
archiveDescriptor.assertOutputFiles(outputDir, INNER_FOLDER_NAME);
58+
}
59+
60+
/** Test decompressing a .7z with entries being renamed during the extraction process. */
61+
@Test
62+
public void testDecompressWithRenamedFiles() throws Exception {
63+
String innerDirName = ROOT_FOLDER_NAME + "/" + INNER_FOLDER_NAME;
64+
65+
HashMap<String, String> renameFiles = new HashMap<>();
66+
renameFiles.put(innerDirName + "/regular_file", innerDirName + "/renamedFile");
67+
DecompressorDescriptor.Builder descriptorBuilder =
68+
archiveDescriptor.createDescriptorBuilder().setRenameFiles(renameFiles);
69+
Path outputDir = decompress(descriptorBuilder.build());
70+
71+
Path innerDir = outputDir.getRelative(ROOT_FOLDER_NAME).getRelative(INNER_FOLDER_NAME);
72+
assertThat(innerDir.getRelative("renamedFile").exists()).isTrue();
73+
}
74+
75+
/** Test that entry renaming is applied prior to prefix stripping. */
76+
@Test
77+
public void testDecompressWithRenamedFilesAndPrefix() throws Exception {
78+
String innerDirName = ROOT_FOLDER_NAME + "/" + INNER_FOLDER_NAME;
79+
80+
HashMap<String, String> renameFiles = new HashMap<>();
81+
renameFiles.put(innerDirName + "/regular_file", innerDirName + "/renamedFile");
82+
DecompressorDescriptor.Builder descriptorBuilder =
83+
archiveDescriptor
84+
.createDescriptorBuilder()
85+
.setPrefix(ROOT_FOLDER_NAME)
86+
.setRenameFiles(renameFiles);
87+
Path outputDir = decompress(descriptorBuilder.build());
88+
89+
Path innerDir = outputDir.getRelative(INNER_FOLDER_NAME);
90+
assertThat(innerDir.getRelative("renamedFile").exists()).isTrue();
91+
}
92+
93+
/** Test that Unicode filenames are handled. **/
94+
@Test
95+
public void testUnicodeFilename() throws Exception {
96+
Path outputDir = decompress(archiveDescriptor.createDescriptorBuilder().build());
97+
98+
Path unicodeFile =
99+
outputDir
100+
.getRelative(ROOT_FOLDER_NAME)
101+
.getRelative(INNER_FOLDER_NAME)
102+
.getRelative(StringEncoding.unicodeToInternal(UNICODE_FILENAME));
103+
assertThat(unicodeFile.exists()).isTrue();
104+
assertThat(unicodeFile.getFileSize()).isNotEqualTo(0);
105+
}
106+
107+
private Path decompress(DecompressorDescriptor descriptor) throws Exception {
108+
return new SevenZDecompressor().decompress(descriptor);
109+
}
110+
}
Binary file not shown.

src/test/tools/bzlmod/MODULE.bazel.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/build_defs/repo/http.bzl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ repository. Files are symlinked after remote files are downloaded and patches (`
337337
By default, the archive type is determined from the file extension of the
338338
URL. If the file has no extension, you can explicitly specify one of the
339339
following: `"zip"`, `"jar"`, `"war"`, `"aar"`, `"tar"`, `"tar.gz"`, `"tgz"`,
340-
`"tar.xz"`, `"txz"`, `"tar.zst"`, `"tzst"`, `"tar.bz2"`, `"ar"`, or `"deb"`.""",
340+
`"tar.xz"`, `"txz"`, `"tar.zst"`, `"tzst"`, `"tar.bz2"`, `"ar"`, `"deb"`, or
341+
`"7z"`.""",
341342
),
342343
"patches": attr.label_list(
343344
default = [],
@@ -450,7 +451,7 @@ and makes its targets available for binding.
450451
451452
It supports the following file extensions: `"zip"`, `"jar"`, `"war"`, `"aar"`, `"tar"`,
452453
`"tar.gz"`, `"tgz"`, `"tar.xz"`, `"txz"`, `"tar.zst"`, `"tzst"`, `tar.bz2`, `"ar"`,
453-
or `"deb"`.
454+
`"deb"`, or `"7z"`.
454455
455456
Examples:
456457
Suppose the current repository contains the source code for a chat program,

0 commit comments

Comments
 (0)