diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index d20361128..5d105d8e0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java @@ -62,7 +62,7 @@ public interface FileAccess { * Creates a symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows * junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, which must * point to absolute paths. Therefore, the created link will be absolute instead of relative. - * + * * @param source the source {@link Path} to link to, may be relative or absolute. * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. * @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute. @@ -73,7 +73,7 @@ public interface FileAccess { * Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a * Windows junction, it will be replaced. In case of missing privileges, Windows Junctions may be used as fallback, * which must point to absolute paths. Therefore, the created link will be absolute instead of relative. - * + * * @param source the source {@link Path} to link to, may be relative or absolute. * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index 08985d6d5..f11248e05 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java @@ -18,6 +18,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -25,12 +27,14 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; @@ -315,8 +319,7 @@ private void deleteLinkIfExists(Path path) throws IOException { return; } } - exists = exists || Files.exists(path); // "||" since broken junctions are not detected by - // Files.exists(brokenJunction) + exists = exists || Files.exists(path); boolean isSymlink = exists && Files.isSymbolicLink(path); assert !(isSymlink && isJunction); @@ -378,7 +381,7 @@ private Path adaptPath(Path source, Path targetLink, boolean relative) throws IO /** * Creates a Windows junction at {@code targetLink} pointing to {@code source}. - * + * * @param source must be another Windows junction or a directory. * @param targetLink the location of the Windows junction. */ @@ -495,12 +498,40 @@ public void untar(Path file, Path targetDir, TarCompression compression) { unpack(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in))); } + /** + * @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file + * permissions of a file on a Unix file system. + * @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--" + */ + public static String generatePermissionString(int permissions) { + + // Ensure that only the last 9 bits are considered + permissions &= 0b111111111; + + StringBuilder permissionStringBuilder = new StringBuilder("rwxrwxrwx"); + + for (int i = 0; i < 9; i++) { + int mask = 1 << i; + char currentChar = ((permissions & mask) != 0) ? permissionStringBuilder.charAt(8 - i) : '-'; + permissionStringBuilder.setCharAt(8 - i, currentChar); + } + + return permissionStringBuilder.toString(); + } + private void unpack(Path file, Path targetDir, Function unpacker) { this.context.trace("Unpacking archive {} to {}", file, targetDir); try (InputStream is = Files.newInputStream(file); ArchiveInputStream ais = unpacker.apply(is)) { ArchiveEntry entry = ais.getNextEntry(); + boolean isTar = ais instanceof TarArchiveInputStream; while (entry != null) { + String permissionStr = null; + if (isTar) { + int tarMode = ((TarArchiveEntry) entry).getMode(); + permissionStr = generatePermissionString(tarMode); + } + Path entryName = Paths.get(entry.getName()); Path entryPath = targetDir.resolve(entryName).toAbsolutePath(); if (!entryPath.startsWith(targetDir)) { @@ -513,6 +544,10 @@ private void unpack(Path file, Path targetDir, Function permissions = PosixFilePermissions.fromString(permissionStr); + Files.setPosixFilePermissions(entryPath, permissions); + } entry = ais.getNextEntry(); } } catch (IOException e) { diff --git a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java index c2f0bae8c..33f2b7183 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.io; +import static com.devonfw.tools.ide.io.FileAccessImpl.generatePermissionString; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; @@ -7,6 +8,9 @@ import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -325,7 +329,7 @@ private void createSymlinks(FileAccess fa, Path dir, boolean relative) { /** * Checks if the symlinks exist. This is used by the tests of {@link FileAccessImpl#symlink(Path, Path, boolean)}. - * + * * @param dir the {@link Path} to the directory where the symlinks are expected. */ private void assertSymlinksExist(Path dir) { @@ -469,4 +473,99 @@ private void assertSymlinkRead(Path link, Path trueTarget) { + " and readPath " + readPath, e); } } + + /** + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} with {@link TarCompression#NONE} and checks if + * file permissions are preserved on Unix. + */ + @Test + public void testUntarWithNoneCompressionWithFilePermissions(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (context.getSystemInfo().isWindows()) { + return; + } + + // act + context.getFileAccess().untar( + Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.tar"), tempDir, + TarCompression.NONE); + + // assert + assertPosixFilePermissions(tempDir.resolve("executableFile.txt"), "rwxrwxr-x"); + assertPosixFilePermissions(tempDir.resolve("nonExecutableFile.txt"), "rw-rw-r--"); + } + + /** + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} with {@link TarCompression#GZ} and checks if file + * permissions are preserved on Unix. + */ + @Test + public void testUntarWithGzCompressionWithFilePermissions(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (context.getSystemInfo().isWindows()) { + return; + } + + // act + context.getFileAccess().untar( + Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.tar.gz"), tempDir, + TarCompression.GZ); + + // assert + assertPosixFilePermissions(tempDir.resolve("executableFile.txt"), "rwxrwxr-x"); + assertPosixFilePermissions(tempDir.resolve("nonExecutableFile.txt"), "rw-rw-r--"); + } + + /** + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} with {@link TarCompression#BZIP2} and checks if + * file permissions are preserved on Unix. + */ + @Test + public void testUntarWithBzip2CompressionWithFilePermissions(@TempDir Path tempDir) { + + // arrange + IdeContext context = IdeTestContextMock.get(); + if (context.getSystemInfo().isWindows()) { + return; + } + + // act + context.getFileAccess().untar( + Path.of("src/test/resources/com/devonfw/tools/ide/io").resolve("executable_and_non_executable.tar.bz2"), + tempDir, TarCompression.BZIP2); + + // assert + assertPosixFilePermissions(tempDir.resolve("executableFile.txt"), "rwxrwxr-x"); + assertPosixFilePermissions(tempDir.resolve("nonExecutableFile.txt"), "rw-rw-r--"); + } + + private void assertPosixFilePermissions(Path file, String permissions) { + + try { + Set posixPermissions = Files.getPosixFilePermissions(file); + String permissionStr = PosixFilePermissions.toString(posixPermissions); + assertThat(permissions).isEqualTo(permissionStr); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Test of {@link FileAccessImpl#generatePermissionString(int)}. + */ + @Test + public void testGeneratePermissionString() { + + assertThat(generatePermissionString(0)).isEqualTo("---------"); + assertThat(generatePermissionString(436)).isEqualTo("rw-rw-r--"); + assertThat(generatePermissionString(948)).isEqualTo("rw-rw-r--"); + assertThat(generatePermissionString(509)).isEqualTo("rwxrwxr-x"); + assertThat(generatePermissionString(511)).isEqualTo("rwxrwxrwx"); + + } + } diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar new file mode 100644 index 000000000..86c1e1761 Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar differ diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.bz2 b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.bz2 new file mode 100644 index 000000000..25fd0d565 Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.bz2 differ diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.gz b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.gz new file mode 100644 index 000000000..29e02dff2 Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.gz differ diff --git a/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip new file mode 100644 index 000000000..855957b5b Binary files /dev/null and b/cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip differ