Skip to content

Commit

Permalink
devonfw#132: untar preserves unix file permissios
Browse files Browse the repository at this point in the history
also wrote a test for untar.
Unzip still does not preserve file permissions
  • Loading branch information
MattesMrzik committed Jan 17, 2024
1 parent 79f4e76 commit 5b01c5f
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 6 deletions.
12 changes: 10 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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.
Expand All @@ -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}.
*/
Expand Down Expand Up @@ -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.
*
Expand Down
44 changes: 41 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,20 +18,25 @@
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.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;

import com.devonfw.tools.ide.context.IdeContext;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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<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;
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)) {
Expand All @@ -513,6 +549,8 @@ private void unpack(Path file, Path targetDir, Function<InputStream, ArchiveInpu
mkdirs(entryPath.getParent());
Files.copy(ais, entryPath);
}
Set<PosixFilePermission> permissions = PosixFilePermissions.fromString(permissionStr);
Files.setPosixFilePermissions(entryPath, permissions);
entry = ais.getNextEntry();
}
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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,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<PosixFilePermission> posixPermissions = Files.getPosixFilePermissions(file);
String permissionStr = PosixFilePermissions.toString(posixPermissions);
assertThat(permissions).isEqualTo(permissionStr);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}
Binary file not shown.
Binary file not shown.

0 comments on commit 5b01c5f

Please sign in to comment.