Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#132: untar preserves unix file permissios #185

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading