From 5b01c5fb4c79a575b0174c0e41a9a8b47e904618 Mon Sep 17 00:00:00 2001 From: MattesMrzik Date: Wed, 17 Jan 2024 15:04:38 +0100 Subject: [PATCH 1/2] #132: untar preserves unix file permissios also wrote a test for untar. Unzip still does not preserve file permissions --- .../com/devonfw/tools/ide/io/FileAccess.java | 12 ++++- .../devonfw/tools/ide/io/FileAccessImpl.java | 44 +++++++++++++++-- .../tools/ide/io/FileAccessImplTest.java | 46 +++++++++++++++++- .../io/executable_and_non_executable.tar.gz | Bin 0 -> 188 bytes .../ide/io/executable_and_non_executable.zip | Bin 0 -> 421 bytes 5 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.gz create mode 100644 cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.zip 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 0000000000000000000000000000000000000000..29e02dff2ccb40f99deecb5692f7db96d99e656f GIT binary patch literal 188 zcmb2|=3oE==C@aTxegf!v^`u?-8IKSXts&^h3YFtO##6hG{5}Vwj$udqh(*&@1+|z zo=g(^-1{Y{bfwLeP4-tedDaGX8#fF5iOBts7M1^L!J5zab|u{XV0P{19nIWn@=qhL zFZ;A;tJQQ(PwW5hWB>by`uYe>&g+YMw_4$AvgoEaw;v~_ZtUE>)&GFR$3+F7f0p0& l{C9Rny}ZuP|1F;X;@2^$LmUVpA8e0h{CG}SCX1nqL81a0MnS71JlUG6#%#D1kkFTNB*x-0$KyY zqCl(i^7CA=+a>|DjqMsf+py{dS;)vF$BfH&5 Date: Thu, 18 Jan 2024 15:19:01 +0100 Subject: [PATCH 2/2] #132: removed experimental zip extraction code, added tests - removed the code I experimented with regarding the Zip extraction, which will probably get addressed in a different PR. - the tar extraction tests now test NONE, GZ, and BZIP2 compression --- .../com/devonfw/tools/ide/io/FileAccess.java | 8 -- .../devonfw/tools/ide/io/FileAccessImpl.java | 25 +++--- .../tools/ide/io/FileAccessImplTest.java | 75 +++++++++++++++--- .../ide/io/executable_and_non_executable.tar | Bin 0 -> 10240 bytes .../io/executable_and_non_executable.tar.bz2 | Bin 0 -> 196 bytes 5 files changed, 76 insertions(+), 32 deletions(-) create mode 100644 cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar create mode 100644 cli/src/test/resources/com/devonfw/tools/ide/io/executable_and_non_executable.tar.bz2 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 8cf032c8a..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 @@ -1,6 +1,5 @@ package com.devonfw.tools.ide.io; -import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import java.nio.file.Path; import java.util.function.Predicate; @@ -125,13 +124,6 @@ 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 c58dbdff8..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 @@ -36,7 +36,6 @@ 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; @@ -499,7 +498,12 @@ public void untar(Path file, Path targetDir, TarCompression compression) { unpack(file, targetDir, in -> new TarArchiveInputStream(compression.unpack(in))); } - public String generatePermissionString(int permissions) { + /** + * @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; @@ -521,18 +525,9 @@ private void unpack(Path file, Path targetDir, Function> 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) { + if (isTar) { int tarMode = ((TarArchiveEntry) entry).getMode(); permissionStr = generatePermissionString(tarMode); } @@ -549,8 +544,10 @@ private void unpack(Path file, Path targetDir, Function permissions = PosixFilePermissions.fromString(permissionStr); - Files.setPosixFilePermissions(entryPath, permissions); + if (isTar) { + Set 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 5f32f583e..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,7 +8,6 @@ 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; @@ -475,20 +475,38 @@ private void assertSymlinkRead(Path link, Path trueTarget) { } /** - * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} and checks if file permissions are preserved on - * Linux + * Test of {@link FileAccessImpl#untar(Path, Path, TarCompression)} with {@link TarCompression#NONE} and checks if + * file permissions are preserved on Unix. */ @Test - public void testUntarWithFilePermissions(@TempDir Path tempDir) { - // TODO test: - // NONE -> not yet checked - // GZ, -> checked - // BZIP2 -> not yet checked + public void testUntarWithNoneCompressionWithFilePermissions(@TempDir Path tempDir) { // arrange IdeContext context = IdeTestContextMock.get(); - // TODO I think these are not relevant on Windows. But what about MacOS? - if (!context.getSystemInfo().isLinux()) { + 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; } @@ -502,6 +520,29 @@ public void testUntarWithFilePermissions(@TempDir Path tempDir) { 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 { @@ -513,4 +554,18 @@ private void assertPosixFilePermissions(Path file, String permissions) { } } + /** + * 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 0000000000000000000000000000000000000000..86c1e1761b4590cefad277b2dfaac60f98781154 GIT binary patch literal 10240 zcmeIyK?;K~5J1taJw;AXlA4$kwD+)*LcvC;5qkVaP{_jKLQ7rzY$8q!!*gc*+c!h3 zZ>_&RT7Qb~D0W{eE$6IAn|4;^`xsKHbV3_zwQ<^{XQhoUjgV^hOcpL2`dGiD6wf+F z-!B}PUaiHye|hLg(}n0ml-*tWsT6!WmqJk-I(L%WE)cqK`Sl1GTfC}F-uLqy7(+^# zbJqW9KBQsbH2uH*@2=mF_5WfiFap*MIe`EI2q1s}0tg_000IagfB*srAbe*Sj~yp96L6qeQmvB(#-vA};i$*BTE;XP0)v%C6$t_OaKNlb y2H2z5>vqRI%wsooSGFo9)x%Z=-3${ylWJ7ly2Y<4ROpC5#oUoj6eKopqcZ@+3{EQm literal 0 HcmV?d00001