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..8cf032c8a 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 @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.io; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import java.nio.file.Path; import java.util.function.Predicate; @@ -62,7 +63,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 +74,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}. */ @@ -124,6 +125,13 @@ default void copy(Path source, Path target) { */ Path toRealPath(Path path); + /** + * @param permissionInt 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--" + */ + String generatePermissionString(int permissionInt); + /** * Deletes the given {@link Path} idempotent and recursive. * 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..c58dbdff8 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,13 +27,16 @@ 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.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; import com.devonfw.tools.ide.context.IdeContext; @@ -315,8 +320,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 +382,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 +499,44 @@ public void untar(Path file, Path targetDir, TarCompression compression) { unpack(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in))); } + public 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; + boolean isZip = ais instanceof ZipArchiveInputStream; while (entry != null) { + String permissionStr = null; + if (isZip) { + // TODO ZipArchiveInputStream is unable to fill this field, you must use ZipFile if you want to read entries + // using this attribute (getExternalAttributes). + int unixMode = ((int) ((ZipArchiveEntry) entry).getExternalAttributes() >> 16) & 0xFFFF; + // unixMode always zero since ZipArchiveInputStream does not read getExternalAttributes() + System.out.println("File: " + ((ZipArchiveEntry) entry).getName()); + System.out.println("Unix Mode: " + unixMode); + System.out.println("Unix Mode octal: " + Integer.toOctalString(unixMode)); + } else 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 +549,8 @@ 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..5f32f583e 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 @@ -7,6 +7,10 @@ import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.Paths; +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,44 @@ private void assertSymlinkRead(Path link, Path trueTarget) { + " and readPath " + readPath, e); } } + + /** + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} and checks if file permissions are preserved on + * Linux + */ + @Test + public void testUntarWithFilePermissions(@TempDir Path tempDir) { + // TODO test: + // NONE -> not yet checked + // GZ, -> checked + // BZIP2 -> not yet checked + + // arrange + IdeContext context = IdeTestContextMock.get(); + // TODO I think these are not relevant on Windows. But what about MacOS? + if (!context.getSystemInfo().isLinux()) { + 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--"); + } + + 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); + } + } + } 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