View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.buildcache;
20  
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.nio.charset.StandardCharsets;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.attribute.PosixFilePermission;
27  import java.nio.file.attribute.PosixFilePermissions;
28  import java.util.Arrays;
29  import java.util.Set;
30  
31  import org.junit.jupiter.api.Test;
32  import org.junit.jupiter.api.io.TempDir;
33  
34  import static org.junit.jupiter.api.Assertions.assertFalse;
35  import static org.junit.jupiter.api.Assertions.assertTrue;
36  
37  /**
38   * Tests for permission preservation in CacheUtils.zip() and CacheUtils.unzip() methods.
39   * These tests verify that Unix file permissions affect ZIP file hashes when preservation
40   * is enabled, and do not affect hashes when disabled.
41   */
42  class CacheUtilsPermissionsTest {
43  
44      @TempDir
45      Path tempDir;
46  
47      /**
48       * Tests that ZIP file hash changes when permissions change (when preservePermissions=true).
49       * This ensures that the cache invalidates when file permissions change, maintaining
50       * cache correctness similar to how Git includes file mode in tree hashes.
51       */
52      @Test
53      void testPermissionsAffectFileHashWhenEnabled() throws IOException {
54          // Skip test on non-POSIX filesystems (e.g., Windows)
55          if (!tempDir.getFileSystem().supportedFileAttributeViews().contains("posix")) {
56              return;
57          }
58  
59          // Given: Same directory content with different permissions
60          Path sourceDir1 = tempDir.resolve("source1");
61          Files.createDirectories(sourceDir1);
62          Path file1 = sourceDir1.resolve("script.sh");
63          writeString(file1, "#!/bin/bash\necho hello");
64  
65          // Set executable permissions (755)
66          Set<PosixFilePermission> execPermissions = PosixFilePermissions.fromString("rwxr-xr-x");
67          Files.setPosixFilePermissions(file1, execPermissions);
68  
69          // Create second directory with identical content but different permissions
70          Path sourceDir2 = tempDir.resolve("source2");
71          Files.createDirectories(sourceDir2);
72          Path file2 = sourceDir2.resolve("script.sh");
73          writeString(file2, "#!/bin/bash\necho hello"); // Identical content
74  
75          // Set non-executable permissions (644)
76          Set<PosixFilePermission> normalPermissions = PosixFilePermissions.fromString("rw-r--r--");
77          Files.setPosixFilePermissions(file2, normalPermissions);
78  
79          // When: Create ZIP files with preservePermissions=true
80          Path zip1 = tempDir.resolve("cache1.zip");
81          Path zip2 = tempDir.resolve("cache2.zip");
82          CacheUtils.zip(sourceDir1, zip1, "*", true);
83          CacheUtils.zip(sourceDir2, zip2, "*", true);
84  
85          // Then: ZIP files should have different hashes despite identical content
86          byte[] hash1 = Files.readAllBytes(zip1);
87          byte[] hash2 = Files.readAllBytes(zip2);
88  
89          boolean hashesAreDifferent = !Arrays.equals(hash1, hash2);
90          assertTrue(
91                  hashesAreDifferent,
92                  "ZIP files with same content but different permissions should have different hashes "
93                          + "when preservePermissions=true. This ensures cache invalidation when permissions change "
94                          + "(executable vs non-executable files).");
95      }
96  
97      /**
98       * Tests that ZIP file hash does NOT significantly vary when permissions change but
99       * preservePermissions=false. While ZIP timestamps may still cause minor differences,
100      * the key point is that permission information is NOT deterministically stored.
101      */
102     @Test
103     void testPermissionsDoNotAffectHashWhenDisabled() throws IOException {
104         // Skip test on non-POSIX filesystems (e.g., Windows)
105         if (!tempDir.getFileSystem().supportedFileAttributeViews().contains("posix")) {
106             return;
107         }
108 
109         // Given: Same directory content with different permissions
110         Path sourceDir1 = tempDir.resolve("source1");
111         Files.createDirectories(sourceDir1);
112         Path file1 = sourceDir1.resolve("script.sh");
113         writeString(file1, "#!/bin/bash\necho hello");
114 
115         // Set executable permissions (755)
116         Set<PosixFilePermission> execPermissions = PosixFilePermissions.fromString("rwxr-xr-x");
117         Files.setPosixFilePermissions(file1, execPermissions);
118 
119         // Create second directory with identical content but different permissions
120         Path sourceDir2 = tempDir.resolve("source2");
121         Files.createDirectories(sourceDir2);
122         Path file2 = sourceDir2.resolve("script.sh");
123         writeString(file2, "#!/bin/bash\necho hello"); // Identical content
124 
125         // Set non-executable permissions (644)
126         Set<PosixFilePermission> normalPermissions = PosixFilePermissions.fromString("rw-r--r--");
127         Files.setPosixFilePermissions(file2, normalPermissions);
128 
129         // When: Create ZIP files with preservePermissions=false
130         Path zip1 = tempDir.resolve("cache1.zip");
131         Path zip2 = tempDir.resolve("cache2.zip");
132         CacheUtils.zip(sourceDir1, zip1, "*", false);
133         CacheUtils.zip(sourceDir2, zip2, "*", false);
134 
135         // Unzip and verify permissions are NOT preserved
136         Path extractDir1 = tempDir.resolve("extracted1");
137         Path extractDir2 = tempDir.resolve("extracted2");
138         Files.createDirectories(extractDir1);
139         Files.createDirectories(extractDir2);
140         CacheUtils.unzip(zip1, extractDir1, false);
141         CacheUtils.unzip(zip2, extractDir2, false);
142 
143         Path extractedFile1 = extractDir1.resolve("script.sh");
144         Path extractedFile2 = extractDir2.resolve("script.sh");
145 
146         Set<PosixFilePermission> perms1 = Files.getPosixFilePermissions(extractedFile1);
147         Set<PosixFilePermission> perms2 = Files.getPosixFilePermissions(extractedFile2);
148 
149         // Files should NOT retain their original different permissions
150         // Both should have default permissions determined by umask
151         assertFalse(
152                 perms1.equals(execPermissions) && perms2.equals(normalPermissions),
153                 "When preservePermissions=false, original permissions should NOT be preserved. "
154                         + "Files should use system default permissions (umask).");
155     }
156 
157     /**
158      * Java 8 compatible version of Files.writeString().
159      */
160     private void writeString(Path path, String content) throws IOException {
161         try (OutputStream out = Files.newOutputStream(path)) {
162             out.write(content.getBytes(StandardCharsets.UTF_8));
163         }
164     }
165 }