diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java index 8a0369e8a..741621c70 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java @@ -24,15 +24,20 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; import java.io.Closeable; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; import java.util.*; -/** - * @author Glavo - */ -public abstract class ArchiveFileTree implements Closeable { +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/// @author Glavo +public abstract class ArchiveFileTree implements Closeable { public static ArchiveFileTree open(Path file) throws IOException { Path namePath = file.getFileName(); @@ -50,23 +55,55 @@ public abstract class ArchiveFileTree implements Clos } } - protected final F file; - protected final Dir root = new Dir<>(""); + protected final R reader; + protected final Dir root = new Dir<>("", ""); - public ArchiveFileTree(F file) { - this.file = file; + public ArchiveFileTree(R reader) { + this.reader = reader; } - public F getFile() { - return file; + public R getReader() { + return reader; } public Dir getRoot() { return root; } + public @Nullable E getEntry(@NotNull String entryPath) { + Dir dir = root; + String fileName; + if (entryPath.indexOf('/') < 0) { + fileName = entryPath; + if (fileName.isEmpty()) + return root.getEntry(); + } else { + String[] path = entryPath.split("/"); + if (path.length == 0) + return root.getEntry(); + + for (int i = 0; i < path.length - 1; i++) { + String item = path[i]; + if (item.isEmpty()) + continue; + dir = dir.getSubDirs().get(item); + if (dir == null) + return null; + } + + fileName = path[path.length - 1]; + E entry = dir.getFiles().get(fileName); + if (entry != null) + return entry; + } + + Dir subDir = dir.getSubDirs().get(fileName); + return subDir != null ? subDir.getEntry() : null; + } + protected void addEntry(E entry) throws IOException { String[] path = entry.getName().split("/"); + List pathList = Arrays.asList(path); Dir dir = root; @@ -81,7 +118,9 @@ public abstract class ArchiveFileTree implements Clos throw new IOException("A file and a directory have the same name: " + entry.getName()); } - dir = dir.subDirs.computeIfAbsent(item, Dir::new); + final int nameEnd = i + 1; + dir = dir.subDirs.computeIfAbsent(item, name -> + new Dir<>(name, String.join("/", pathList.subList(0, nameEnd)))); } if (entry.isDirectory()) { @@ -106,6 +145,55 @@ public abstract class ArchiveFileTree implements Clos public abstract InputStream getInputStream(E entry) throws IOException; + public @NotNull InputStream getInputStream(String entryPath) throws IOException { + E entry = getEntry(entryPath); + if (entry == null) + throw new FileNotFoundException("Entry not found: " + entryPath); + return getInputStream(entry); + } + + public byte[] readBinaryEntry(@NotNull E entry) throws IOException { + try (InputStream input = getInputStream(entry)) { + return input.readAllBytes(); + } + } + + public String readTextEntry(@NotNull String entryPath) throws IOException { + E entry = getEntry(entryPath); + if (entry == null) + throw new FileNotFoundException("Entry not found: " + entryPath); + return readTextEntry(entry); + } + + public String readTextEntry(@NotNull E entry) throws IOException { + return new String(readBinaryEntry(entry), StandardCharsets.UTF_8); + } + + protected void copyAttributes(@NotNull E source, @NotNull Path targetFile) throws IOException { + FileTime lastModifiedTime = source.getLastModifiedTime(); + if (lastModifiedTime != null) + Files.setLastModifiedTime(targetFile, lastModifiedTime); + } + + public void extractTo(@NotNull String entryPath, @NotNull Path targetFile) throws IOException { + E entry = getEntry(entryPath); + if (entry == null) + throw new FileNotFoundException("Entry not found: " + entryPath); + + extractTo(entry, targetFile); + } + + public void extractTo(@NotNull E entry, @NotNull Path targetFile) throws IOException { + try (InputStream input = getInputStream(entry)) { + Files.copy(input, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + try { + copyAttributes(entry, targetFile); + } catch (Throwable e) { + LOG.warning("Failed to copy attributes to " + targetFile, e); + } + } + public abstract boolean isLink(E entry); public abstract String getLink(E entry) throws IOException; @@ -117,13 +205,15 @@ public abstract class ArchiveFileTree implements Clos public static final class Dir { private final String name; + private final String fullName; private E entry; final Map> subDirs = new HashMap<>(); final Map files = new HashMap<>(); - public Dir(String name) { + public Dir(String name, String fullName) { this.name = name; + this.fullName = fullName; } public boolean isRoot() { @@ -134,6 +224,11 @@ public abstract class ArchiveFileTree implements Clos return name; } + /// Get the normalized full path. Leading `/` and all `.` in the path will be removed. + public @NotNull String getFullName() { + return fullName; + } + public @Nullable E getEntry() { return entry; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java index 0b7cdd359..f41e8ffd2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java @@ -20,11 +20,13 @@ package org.jackhuang.hmcl.util.tree; import kala.compress.archivers.tar.TarArchiveEntry; import kala.compress.archivers.tar.TarArchiveReader; +import org.jetbrains.annotations.NotNull; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributeView; import java.util.zip.GZIPInputStream; /** @@ -98,9 +100,22 @@ public final class TarFileTree extends ArchiveFileTree permissions = EnumSet.noneOf(PosixFilePermission.class); + + // Owner permissions + if ((unixMode & 0400) != 0) permissions.add(PosixFilePermission.OWNER_READ); + if ((unixMode & 0200) != 0) permissions.add(PosixFilePermission.OWNER_WRITE); + if ((unixMode & 0100) != 0) permissions.add(PosixFilePermission.OWNER_EXECUTE); + + // Group permissions + if ((unixMode & 0040) != 0) permissions.add(PosixFilePermission.GROUP_READ); + if ((unixMode & 0020) != 0) permissions.add(PosixFilePermission.GROUP_WRITE); + if ((unixMode & 0010) != 0) permissions.add(PosixFilePermission.GROUP_EXECUTE); + + // Others permissions + if ((unixMode & 0004) != 0) permissions.add(PosixFilePermission.OTHERS_READ); + if ((unixMode & 0002) != 0) permissions.add(PosixFilePermission.OTHERS_WRITE); + if ((unixMode & 0001) != 0) permissions.add(PosixFilePermission.OTHERS_EXECUTE); + + posixView.setPermissions(permissions); + } } @Override public InputStream getInputStream(ZipArchiveEntry entry) throws IOException { - return getFile().getInputStream(entry); + return getReader().getInputStream(entry); } @Override @@ -66,7 +115,7 @@ public final class ZipFileTree extends ArchiveFileTree