Skip to content

Commit 298f064

Browse files
committed
Preserve Unix file permissions when caching attachedOutputs
Fixes apache#214 ## Problem When using attachedOutputs, executable file permissions are lost during cache restoration. This is because the standard java.util.zip classes do not preserve Unix file permissions. ## Solution Switch from java.util.zip to Apache Commons Compress's ZipArchive classes which support Unix permissions via setUnixMode() and getUnixMode() methods. ## Changes 1. **Add Apache Commons Compress 1.28.0 dependency** (was already transitive test dependency, now direct compile dependency) 2. **Replace java.util.zip with Commons Compress in CacheUtils**: - ZipEntry → ZipArchiveEntry - ZipOutputStream → ZipArchiveOutputStream - ZipInputStream → ZipArchiveInputStream 3. **Preserve permissions during zip**: - Read POSIX permissions with Files.getPosixFilePermissions() - Convert to Unix mode integer - Store via zipEntry.setUnixMode() 4. **Restore permissions during unzip**: - Read Unix mode from zipEntry.getUnixMode() - Convert to POSIX permissions - Apply via Files.setPosixFilePermissions() 5. **Platform safety**: - Wrap permission operations in try-catch for UnsupportedOperationException - Gracefully handle non-POSIX filesystems (Windows, FAT32, etc.) ## Why Apache Commons Compress? Apache Commons Compress was already a transitive test dependency. Using it provides a clean, simple solution compared to alternatives: **With Commons Compress** (this PR): - 2 lines to preserve permissions: entry.setUnixMode() / entry.getUnixMode() - Well-tested, maintained by Apache - Handles all edge cases and platform differences - Same Apache 2.0 license **Without Commons Compress** (JDK-only approach): - Would require manually encoding Unix permissions in ZipEntry extra field - Complex binary format (InfoZIP extra field specification) - Error-prone and hard to maintain - Platform-specific quirks - No standard API - would need reflection or custom binary encoding ## Testing The changes preserve backward compatibility: - Files without Unix mode (mode=0) are unchanged - Non-POSIX systems gracefully skip permission operations - Existing zip files without permission data work as before Tested scenarios: - Executable shell scripts (rwxr-xr-x / 0755) - Read-only files (r--r--r-- / 0444) - Regular files (rw-r--r-- / 0644) - Windows filesystems (permissions skipped gracefully)
1 parent 8615880 commit 298f064

File tree

2 files changed

+117
-10
lines changed

2 files changed

+117
-10
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ under the License.
139139
<artifactId>commons-io</artifactId>
140140
<version>2.20.0</version>
141141
</dependency>
142+
<dependency>
143+
<groupId>org.apache.commons</groupId>
144+
<artifactId>commons-compress</artifactId>
145+
<version>1.28.0</version>
146+
</dependency>
142147
<dependency>
143148
<groupId>javax.annotation</groupId>
144149
<artifactId>javax.annotation-api</artifactId>

src/main/java/org/apache/maven/buildcache/CacheUtils.java

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,19 @@
2929
import java.nio.file.StandardCopyOption;
3030
import java.nio.file.attribute.BasicFileAttributes;
3131
import java.nio.file.attribute.FileTime;
32+
import java.nio.file.attribute.PosixFilePermission;
33+
import java.nio.file.attribute.PosixFilePermissions;
3234
import java.util.Arrays;
3335
import java.util.Collection;
36+
import java.util.HashSet;
3437
import java.util.List;
3538
import java.util.NoSuchElementException;
39+
import java.util.Set;
3640
import java.util.stream.Stream;
37-
import java.util.zip.ZipEntry;
38-
import java.util.zip.ZipInputStream;
39-
import java.util.zip.ZipOutputStream;
41+
42+
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
43+
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
44+
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
4045

4146
import org.apache.commons.lang3.StringUtils;
4247
import org.apache.commons.lang3.Strings;
@@ -159,7 +164,7 @@ public static boolean isArchive(File file) {
159164
*/
160165
public static boolean zip(final Path dir, final Path zip, final String glob) throws IOException {
161166
final MutableBoolean hasFiles = new MutableBoolean();
162-
try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(zip))) {
167+
try (ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(Files.newOutputStream(zip))) {
163168

164169
PathMatcher matcher =
165170
"*".equals(glob) ? null : FileSystems.getDefault().getPathMatcher("glob:" + glob);
@@ -170,12 +175,21 @@ public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttribu
170175
throws IOException {
171176

172177
if (matcher == null || matcher.matches(path.getFileName())) {
173-
final ZipEntry zipEntry =
174-
new ZipEntry(dir.relativize(path).toString());
175-
zipOutputStream.putNextEntry(zipEntry);
178+
final ZipArchiveEntry zipEntry =
179+
new ZipArchiveEntry(dir.relativize(path).toString());
180+
181+
// Preserve Unix permissions if available
182+
try {
183+
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
184+
zipEntry.setUnixMode(permissionsToMode(permissions));
185+
} catch (UnsupportedOperationException e) {
186+
// Not a POSIX filesystem, permissions not available
187+
}
188+
189+
zipOutputStream.putArchiveEntry(zipEntry);
176190
Files.copy(path, zipOutputStream);
177191
hasFiles.setTrue();
178-
zipOutputStream.closeEntry();
192+
zipOutputStream.closeArchiveEntry();
179193
}
180194
return FileVisitResult.CONTINUE;
181195
}
@@ -185,8 +199,8 @@ public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttribu
185199
}
186200

187201
public static void unzip(Path zip, Path out) throws IOException {
188-
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(zip))) {
189-
ZipEntry entry = zis.getNextEntry();
202+
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(Files.newInputStream(zip))) {
203+
ZipArchiveEntry entry = zis.getNextEntry();
190204
while (entry != null) {
191205
Path file = out.resolve(entry.getName());
192206
if (!file.normalize().startsWith(out.normalize())) {
@@ -200,6 +214,18 @@ public static void unzip(Path zip, Path out) throws IOException {
200214
Files.copy(zis, file, StandardCopyOption.REPLACE_EXISTING);
201215
}
202216
Files.setLastModifiedTime(file, FileTime.fromMillis(entry.getTime()));
217+
218+
// Restore Unix permissions if available
219+
int unixMode = entry.getUnixMode();
220+
if (unixMode != 0) {
221+
try {
222+
Set<PosixFilePermission> permissions = modeToPermissions(unixMode);
223+
Files.setPosixFilePermissions(file, permissions);
224+
} catch (UnsupportedOperationException e) {
225+
// Not a POSIX filesystem, cannot set permissions
226+
}
227+
}
228+
203229
entry = zis.getNextEntry();
204230
}
205231
}
@@ -217,4 +243,80 @@ public static <T> void debugPrintCollection(
217243
}
218244
}
219245
}
246+
247+
/**
248+
* Convert POSIX file permissions to Unix mode integer.
249+
*
250+
* @param permissions POSIX file permissions
251+
* @return Unix mode as integer (e.g., 0755)
252+
*/
253+
private static int permissionsToMode(Set<PosixFilePermission> permissions) {
254+
int mode = 0;
255+
if (permissions.contains(PosixFilePermission.OWNER_READ)) {
256+
mode |= 0400;
257+
}
258+
if (permissions.contains(PosixFilePermission.OWNER_WRITE)) {
259+
mode |= 0200;
260+
}
261+
if (permissions.contains(PosixFilePermission.OWNER_EXECUTE)) {
262+
mode |= 0100;
263+
}
264+
if (permissions.contains(PosixFilePermission.GROUP_READ)) {
265+
mode |= 0040;
266+
}
267+
if (permissions.contains(PosixFilePermission.GROUP_WRITE)) {
268+
mode |= 0020;
269+
}
270+
if (permissions.contains(PosixFilePermission.GROUP_EXECUTE)) {
271+
mode |= 0010;
272+
}
273+
if (permissions.contains(PosixFilePermission.OTHERS_READ)) {
274+
mode |= 0004;
275+
}
276+
if (permissions.contains(PosixFilePermission.OTHERS_WRITE)) {
277+
mode |= 0002;
278+
}
279+
if (permissions.contains(PosixFilePermission.OTHERS_EXECUTE)) {
280+
mode |= 0001;
281+
}
282+
return mode;
283+
}
284+
285+
/**
286+
* Convert Unix mode integer to POSIX file permissions.
287+
*
288+
* @param mode Unix mode (e.g., 0755)
289+
* @return Set of POSIX file permissions
290+
*/
291+
private static Set<PosixFilePermission> modeToPermissions(int mode) {
292+
Set<PosixFilePermission> permissions = new HashSet<>();
293+
if ((mode & 0400) != 0) {
294+
permissions.add(PosixFilePermission.OWNER_READ);
295+
}
296+
if ((mode & 0200) != 0) {
297+
permissions.add(PosixFilePermission.OWNER_WRITE);
298+
}
299+
if ((mode & 0100) != 0) {
300+
permissions.add(PosixFilePermission.OWNER_EXECUTE);
301+
}
302+
if ((mode & 0040) != 0) {
303+
permissions.add(PosixFilePermission.GROUP_READ);
304+
}
305+
if ((mode & 0020) != 0) {
306+
permissions.add(PosixFilePermission.GROUP_WRITE);
307+
}
308+
if ((mode & 0010) != 0) {
309+
permissions.add(PosixFilePermission.GROUP_EXECUTE);
310+
}
311+
if ((mode & 0004) != 0) {
312+
permissions.add(PosixFilePermission.OTHERS_READ);
313+
}
314+
if ((mode & 0002) != 0) {
315+
permissions.add(PosixFilePermission.OTHERS_WRITE);
316+
}
317+
if ((mode & 0001) != 0) {
318+
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
319+
}
320+
return permissions;
321+
}
220322
}

0 commit comments

Comments
 (0)