Skip to content

Commit

Permalink
#132: untar preserves unix file permissios (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
MattesMrzik authored Jan 26, 2024
1 parent 4327271 commit e180912
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 6 deletions.
4 changes: 2 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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}.
*/
Expand Down
41 changes: 38 additions & 3 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,23 @@
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;
import java.time.LocalDateTime;
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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<InputStream, ArchiveInputStream> 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)) {
Expand All @@ -513,6 +544,10 @@ private void unpack(Path file, Path targetDir, Function<InputStream, ArchiveInpu
mkdirs(entryPath.getParent());
Files.copy(ais, entryPath);
}
if (isTar) {
Set<PosixFilePermission> permissions = PosixFilePermissions.fromString(permissionStr);
Files.setPosixFilePermissions(entryPath, permissions);
}
entry = ais.getNextEntry();
}
} catch (IOException e) {
Expand Down
101 changes: 100 additions & 1 deletion cli/src/test/java/com/devonfw/tools/ide/io/FileAccessImplTest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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;
import java.nio.file.Files;
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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<PosixFilePermission> 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");

}

}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit e180912

Please sign in to comment.