package org.graalvm.component.installer.commands;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.ByteChannel;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.graalvm.component.installer.Archive;
import org.graalvm.component.installer.CommonConstants;
import org.graalvm.component.installer.ComponentCollection;
import org.graalvm.component.installer.model.ComponentRegistry;
import org.graalvm.component.installer.Feedback;
import org.graalvm.component.installer.FileOperations;
import org.graalvm.component.installer.SystemUtils;
import org.graalvm.component.installer.model.ComponentInfo;
import org.graalvm.component.installer.model.Verifier;
public class Installer extends AbstractInstaller {
private static final Logger LOG = Logger.getLogger(Installer.class.getName());
private static final Set<PosixFilePermission> DEFAULT_CHANGE_PERMISSION = EnumSet.of(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE,
PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE,
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE);
private final List<Path> filesToDelete = new ArrayList<>();
private final List<Path> dirsToDelete = new ArrayList<>();
private boolean allowFilesInComponentDir;
private boolean rebuildPolyglot;
private final Set<Path> visitedPaths = new HashSet<>();
public Installer(Feedback feedback, FileOperations fileOps, ComponentInfo componentInfo, ComponentRegistry registry, ComponentCollection collection, Archive a) {
super(feedback, fileOps, componentInfo, registry, collection, a);
}
public boolean isAllowFilesInComponentDir() {
return allowFilesInComponentDir;
}
public void setAllowFilesInComponentDir(boolean allowFilesInComponentDir) {
this.allowFilesInComponentDir = allowFilesInComponentDir;
}
@Override
public void revertInstall() {
if (isDryRun()) {
return;
}
LOG.fine("Reverting installation");
for (Path p : filesToDelete) {
try {
LOG.log(Level.FINE, "Deleting: {0}", p);
feedback.verboseOutput("INSTALL_CleanupFile", p);
fileOps.deleteFile(p);
} catch (IOException ex) {
feedback.error("INSTALL_CannotCleanupFile", ex, p, ex.getLocalizedMessage());
}
}
Collections.reverse(dirsToDelete);
for (Path p : dirsToDelete) {
try {
LOG.log(Level.FINE, "Deleting directory: {0}", p);
feedback.verboseOutput("INSTALL_CleanupDirectory", p);
fileOps.deleteFile(p);
} catch (IOException ex) {
feedback.error("INSTALL_CannotCleanupFile", ex, p, ex.getLocalizedMessage());
}
}
}
Path translateTargetPath(Archive.FileEntry entry) {
return translateTargetPath(entry.getName());
}
Path translateTargetPath(String n) {
return translateTargetPath(null, n);
}
Path translateTargetPath(Path base, String n) {
Path rel;
rel = SystemUtils.fromCommonRelative(base, n);
Path p = getInstallPath().resolve(rel).normalize();
if (!p.startsWith(getInstallPath())) {
throw new IllegalStateException(
feedback.l10n("INSTALL_WriteOutsideGraalvm", p));
}
return p;
}
@Override
public boolean validateAll() throws IOException {
Verifier veri = validateRequirements();
ComponentInfo existing = registry.findComponent(componentInfo.getId());
if (existing != null) {
if (!veri.shouldInstall(componentInfo)) {
return false;
}
}
validateFiles();
validateSymlinks();
return true;
}
@Override
public void validateFiles() throws IOException {
if (archive == null) {
throw new UnsupportedOperationException();
}
for (Archive.FileEntry entry : archive) {
if (entry.getName().startsWith("META-INF")) {
continue;
}
feedback.verboseOutput("INSTALL_VerboseValidation", entry.getName());
validateOneEntry(translateTargetPath(entry), entry);
}
}
@Override
public void validateSymlinks() throws IOException {
Map<String, String> processSymlinks = getSymlinks();
for (String sl : processSymlinks.keySet()) {
Path target = fileOps.materialize(translateTargetPath(sl), true);
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
checkLinkReplacement(target,
translateTargetPath(target, processSymlinks.get(sl)));
}
}
}
boolean validateOneEntry(Path target, Archive.FileEntry entry) throws IOException {
if (entry.isDirectory()) {
Path dirPath = fileOps.materialize(SystemUtils.resolveRelative(getInstallPath(), entry.getName()), false);
if (Files.exists(dirPath)) {
if (!Files.isDirectory(dirPath)) {
throw new IOException(
feedback.l10n("INSTALL_OverwriteWithDirectory", dirPath));
}
}
return true;
}
Path mt = fileOps.materialize(target, false);
boolean existingFile = mt != null && Files.exists(mt, LinkOption.NOFOLLOW_LINKS);
if (existingFile) {
return checkFileReplacement(mt, entry);
}
return false;
}
public void install() throws IOException {
assert archive != null : "Must first download / set jar file";
installContent();
installFinish();
}
void installContent() throws IOException {
if (archive == null) {
throw new UnsupportedOperationException();
}
unpackFiles();
archive.completeMetadata(componentInfo);
processPermissions();
createSymlinks();
List<String> ll = new ArrayList<>(getTrackedPaths());
Collections.sort(ll);
componentInfo.setPaths(ll);
rebuildPolyglot = componentInfo.isPolyglotRebuild() ||
ll.stream().filter(p -> p.startsWith(CommonConstants.PATH_POLYGLOT_REGISTRY))
.findAny()
.isPresent();
}
void installFinish() throws IOException {
if (!isDryRun()) {
registry.addComponent(getComponentInfo());
}
}
void unpackFiles() throws IOException {
final String storagePrefix = CommonConstants.PATH_COMPONENT_STORAGE + "/";
for (Archive.FileEntry entry : archive) {
String path = entry.getName();
if (!allowFilesInComponentDir && path.startsWith(storagePrefix) && path.length() > storagePrefix.length()) {
if (path.indexOf('/', storagePrefix.length()) == -1) {
continue;
}
}
installOneEntry(entry);
}
}
void ensurePathExists(Path targetPath) throws IOException {
if (!visitedPaths.add(targetPath)) {
return;
}
Path parent = getInstallPath();
if (!targetPath.normalize().startsWith(parent)) {
throw new IllegalStateException(
feedback.l10n("INSTALL_WriteOutsideGraalvm", targetPath));
}
Path relative = getInstallPath().relativize(targetPath);
Path relativeSubpath;
int count = 0;
for (Path n : relative) {
count++;
relativeSubpath = relative.subpath(0, count);
Path dir = fileOps.materialize(parent.resolve(n), true);
String pathString = SystemUtils.toCommonPath(relativeSubpath) + "/";
if (!Files.exists(dir) || getComponentDirectories().contains(pathString)) {
feedback.verboseOutput("INSTALL_CreatingDirectory", dir);
dirsToDelete.add(dir);
addTrackedPath(pathString);
if (!Files.exists(dir)) {
if (!isDryRun()) {
Files.createDirectory(dir);
}
}
}
parent = dir;
}
}
Path installOneEntry(Archive.FileEntry entry) throws IOException {
if (entry.getName().startsWith("META-INF")) {
return null;
}
Path targetPath = translateTargetPath(entry);
boolean b = validateOneEntry(targetPath, entry);
if (entry.isDirectory()) {
ensurePathExists(targetPath);
return targetPath;
} else {
String eName = entry.getName();
if (b) {
feedback.verboseOutput("INSTALL_SkipIdenticalFile", eName);
return targetPath;
}
return installOneFile(targetPath, entry);
}
}
Path installOneFile(Path target, Archive.FileEntry entry) throws IOException {
try (InputStream jarStream = archive.getInputStream(entry)) {
Path mt = fileOps.materialize(target, false);
Path mt2 = fileOps.materialize(target, true);
boolean existingFile = mt != null && Files.exists(mt, LinkOption.NOFOLLOW_LINKS);
String eName = entry.getName();
if (existingFile) {
feedback.verboseOutput("INSTALL_ReplacingFile", eName);
} else {
filesToDelete.add(mt2);
feedback.verboseOutput("INSTALL_InstallingFile", eName);
}
ensurePathExists(target.getParent());
addTrackedPath(SystemUtils.toCommonPath(getInstallPath().relativize(target)));
if (!isDryRun()) {
fileOps.installFile(target, jarStream);
}
}
return target;
}
@Override
public void processPermissions() throws IOException {
Map<String, String> setPermissions = getPermissions();
List<String> paths = new ArrayList<>(setPermissions.keySet());
Collections.sort(paths);
for (String s : paths) {
Path target = getInstallPath().resolve(SystemUtils.fromCommonRelative(s));
String permissionString = setPermissions.get(s);
Set<PosixFilePermission> perms;
if (permissionString != null && !"".equals(permissionString)) {
perms = PosixFilePermissions.fromString(permissionString);
} else {
perms = DEFAULT_CHANGE_PERMISSION;
}
if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
fileOps.setPermissions(target, perms);
}
}
}
@Override
public void createSymlinks() throws IOException {
if (SystemUtils.isWindows()) {
return;
}
Map<String, String> makeSymlinks = getSymlinks();
List<String> createdRelativeLinks = new ArrayList<>();
try {
List<String> paths = new ArrayList<>(makeSymlinks.keySet());
Collections.sort(paths);
Path instDir = getInstallPath();
for (String s : paths) {
Path source = instDir.resolve(SystemUtils.fromCommonRelative(s));
if (source == null) {
continue;
}
Path parent = source.getParent();
if (parent == null) {
continue;
}
Path target = SystemUtils.fromCommonString(makeSymlinks.get(s));
Path result = parent.resolve(target);
if (result == null) {
continue;
}
result = result.normalize();
if (!result.startsWith(getInstallPath())) {
throw new IllegalStateException(
feedback.l10n("INSTALL_SymlinkOutsideGraalvm", source, result));
}
ensurePathExists(source.getParent());
createdRelativeLinks.add(s);
addTrackedPath(s);
if (Files.exists(source, LinkOption.NOFOLLOW_LINKS)) {
if (checkLinkReplacement(source, target)) {
feedback.verboseOutput("INSTALL_SkipIdenticalFile", s);
filesToDelete.add(source);
continue;
} else {
feedback.verboseOutput("INSTALL_ReplacingFile", s);
Files.delete(source);
}
}
filesToDelete.add(source);
feedback.verboseOutput("INSTALL_CreatingSymlink", s, makeSymlinks.get(s));
if (!isDryRun()) {
Files.createSymbolicLink(source, target);
}
}
} catch (UnsupportedOperationException ex) {
LOG.log(Level.INFO, "Symlinks not supported", ex);
}
componentInfo.addPaths(createdRelativeLinks);
}
boolean checkLinkReplacement(Path existingPath, Path target) throws IOException {
boolean replace = isReplaceDiferentFiles();
if (Files.exists(existingPath, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isSymbolicLink(existingPath)) {
if (Files.isRegularFile(existingPath) && replace) {
return false;
}
throw new IOException(
feedback.l10n("INSTALL_OverwriteWithLink", existingPath));
}
}
Path p = Files.readSymbolicLink(existingPath);
if (!target.equals(p)) {
if (replace) {
return false;
}
throw feedback.failure("INSTALL_ReplacedFileDiffers", null, existingPath);
}
return true;
}
boolean checkFileReplacement(Path existingPath, Archive.FileEntry entry) throws IOException {
boolean replace = isReplaceDiferentFiles();
if (Files.isDirectory(existingPath)) {
throw new IOException(
feedback.l10n("INSTALL_OverwriteWithFile", existingPath));
}
if (!Files.isRegularFile(existingPath) || (Files.size(existingPath) != entry.getSize())) {
if (replace) {
return false;
}
throw feedback.failure("INSTALL_ReplacedFileDiffers", null, existingPath);
}
try (ByteChannel is = Files.newByteChannel(existingPath)) {
if (!archive.checkContentsMatches(is, entry)) {
if (replace) {
return false;
}
throw feedback.failure("INSTALL_ReplacedFileDiffers", null, existingPath);
}
}
return true;
}
@Override
public boolean isRebuildPolyglot() {
return rebuildPolyglot;
}
@Override
public String toString() {
return "Installer[" + componentInfo.getId() + ":" + componentInfo.getName() + "=" + componentInfo.getVersion().displayString() + "]";
}
}