package org.graalvm.component.installer.persist;
import java.io.BufferedReader;
import org.graalvm.component.installer.MetadataException;
import org.graalvm.component.installer.InstallerStopException;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.Function;
import org.graalvm.component.installer.Archive;
import org.graalvm.component.installer.BundleConstants;
import org.graalvm.component.installer.FailedOperationException;
import org.graalvm.component.installer.Feedback;
import org.graalvm.component.installer.SystemUtils;
import org.graalvm.component.installer.model.ComponentInfo;
import org.graalvm.component.installer.model.DistributionType;
public class ComponentPackageLoader implements Closeable, MetadataLoader {
protected final Feedback feedback;
private final Function<String, String> valueSupplier;
private boolean infoOnly;
private final List<InstallerStopException> errors = new ArrayList<>();
private final List<String> fileList = new ArrayList<>();
private String id;
private String version;
private String name;
private String licensePath;
private String licenseType;
private ComponentInfo info;
private boolean noVerifySymlinks;
private final String componentTag;
private final Properties props = new Properties();
static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.graalvm.component.installer.persist.Bundle");
public ComponentPackageLoader(String tag, Function<String, String> supplier, Feedback feedback) {
this.feedback = feedback.withBundle(ComponentPackageLoader.class);
this.valueSupplier = supplier;
this.componentTag = tag;
}
public ComponentPackageLoader(Function<String, String> supplier, Feedback feedback) {
this(null, supplier, feedback);
}
@Override
public Archive getArchive() {
return null;
}
private String value(String key) {
String v = valueSupplier.apply(key);
if (v != null && (componentTag == null || componentTag.isEmpty())) {
props.put(key, v);
}
return v;
}
@Override
public ComponentPackageLoader infoOnly(boolean only) {
this.infoOnly = only;
return this;
}
private HeaderParser (String header) throws MetadataException {
return parseHeader2(header, null);
}
private HeaderParser (String header, Function<String, String> fn) throws MetadataException {
String s = value(header);
if (fn != null) {
s = fn.apply(s);
}
return new HeaderParser(header, s, feedback).mustExist();
}
private HeaderParser (String header, String defValue) throws MetadataException {
String s = value(header);
if (s == null) {
if (defValue == null) {
return new HeaderParser(header, s, feedback);
} else {
return new HeaderParser(header, defValue, feedback);
}
}
return new HeaderParser(header, s, feedback).mustExist();
}
@Override
public ComponentInfo getComponentInfo() {
if (info == null) {
return createComponentInfo();
}
return info;
}
private void parse(Runnable... parts) {
for (Runnable r : parts) {
try {
r.run();
} catch (MetadataException ex) {
if (BundleConstants.BUNDLE_ID.equals(ex.getOffendingHeader())) {
throw ex;
}
if (infoOnly) {
errors.add(ex);
} else {
throw ex;
}
} catch (InstallerStopException ex) {
if (infoOnly) {
errors.add(ex);
} else {
throw ex;
}
}
}
}
@Override
public List<InstallerStopException> getErrors() {
return errors;
}
private void supplyComponentTag() {
String ct = info.getTag();
if (ct != null && !ct.isEmpty()) {
return;
}
try (StringWriter wr = new StringWriter()) {
props.store(wr, "");
info.setTag(SystemUtils.digestString(wr.toString().replaceAll("#.*\r?\n\r?", ""), false));
} catch (IOException ex) {
throw new FailedOperationException(ex.getLocalizedMessage(), ex);
}
}
private void loadWorkingDirectories(ComponentInfo nfo) {
String val = parseHeader(BundleConstants.BUNDLE_WORKDIRS, null).getContents("");
Set<String> workDirs = new LinkedHashSet<>();
for (String s : val.split(":")) {
String p = s.trim();
if (!p.isEmpty()) {
workDirs.add(p);
}
}
nfo.addWorkingDirectories(workDirs);
}
private String findComponentTag() {
String t = value(BundleConstants.BUNDLE_SERIAL);
return t != null && !t.isEmpty() ? t : componentTag;
}
protected ComponentInfo createBaseComponentInfo() {
parse(
() -> id = parseHeader(BundleConstants.BUNDLE_ID).parseSymbolicName(),
() -> name = parseHeader(BundleConstants.BUNDLE_NAME).getContents(id),
() -> version = parseHeader(BundleConstants.BUNDLE_VERSION).version(),
() -> {
info = new ComponentInfo(id, name, version, findComponentTag());
info.addRequiredValues(parseHeader(BundleConstants.BUNDLE_REQUIRED).parseRequiredCapabilities());
info.addProvidedValues(parseHeader(BundleConstants.BUNDLE_PROVIDED, "").parseProvidedCapabilities());
info.setDependencies(parseHeader(BundleConstants.BUNDLE_DEPENDENCY, "").parseDependencies());
info.setStability(parseHeader(BundleConstants.BUNDLE_STABILITY, "").parseStability());
});
supplyComponentTag();
return info;
}
protected ComponentInfo loadExtendedMetadata(ComponentInfo base) {
parse(
() -> base.setPolyglotRebuild(parseHeader(BundleConstants.BUNDLE_POLYGLOT_PART, null).getBoolean(Boolean.FALSE)),
() -> base.setDistributionType(parseDistributionType()),
() -> loadWorkingDirectories(base),
() -> loadMessages(base),
() -> loadLicenseType(base));
return base;
}
public ComponentInfo createComponentInfo() {
ComponentInfo nfo = createBaseComponentInfo();
return loadExtendedMetadata(nfo);
}
private DistributionType parseDistributionType() {
String dtString = parseHeader(BundleConstants.BUNDLE_COMPONENT_DISTRIBUTION, null).getContents(DistributionType.OPTIONAL.name());
try {
return DistributionType.valueOf(dtString.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException ex) {
throw new MetadataException(BundleConstants.BUNDLE_COMPONENT_DISTRIBUTION,
feedback.l10n("ERROR_InvalidDistributionType", dtString));
}
}
private void loadLicenseType(ComponentInfo nfo) {
licenseType = parseHeader(BundleConstants.BUNDLE_LICENSE_TYPE, null).getContents(null);
nfo.setLicenseType(licenseType);
if (licenseType != null) {
licensePath = parseHeader(BundleConstants.BUNDLE_LICENSE_PATH).mustExist().getContents(null);
nfo.setLicensePath(licensePath);
}
}
@Override
public String getLicensePath() {
if (info != null) {
return info.getLicensePath();
}
return licensePath;
}
private String cachedLicenseID;
@Override
public String getLicenseID() {
if (cachedLicenseID != null) {
return cachedLicenseID;
}
String licPath = getLicensePath();
if (licPath == null) {
return null;
} else if (SystemUtils.isRemotePath(licPath)) {
return licPath;
}
Archive.FileEntry foundEntry = null;
for (Archive.FileEntry fe : getArchive()) {
if (getLicensePath().equals(fe.getName())) {
foundEntry = fe;
break;
}
}
if (foundEntry == null) {
throw feedback.failure("ERROR_CannotComputeLicenseID", null, licPath);
}
ByteBuffer bb = ByteBuffer.allocate(Integer.getInteger("org.graalvm.component.installer.fileReadBuffer", 4096));
MessageDigest dg;
String licId;
try {
dg = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ex) {
throw feedback.failure("ERROR_CannotComputeLicenseID", ex, foundEntry.getName());
}
try (InputStream is = getArchive().getInputStream(foundEntry);
ReadableByteChannel rch = Channels.newChannel(is)) {
while (true) {
int read = rch.read(bb);
if (read < 0) {
break;
}
bb.flip();
dg.update(bb);
bb.clear();
}
licId = SystemUtils.fingerPrint(dg.digest(), false);
} catch (IOException ex) {
throw feedback.failure("ERROR_CannotComputeLicenseID", ex, foundEntry.getName());
}
return cachedLicenseID = licId;
}
@Override
public String getLicenseType() {
return licenseType;
}
private void throwInvalidPermissions() {
throw feedback.failure("ERROR_PermissionFormat", null);
}
@SuppressWarnings("unchecked")
protected Map<String, String> parsePermissions(BufferedReader r) throws IOException {
Map<String, String> result = new LinkedHashMap<>();
Properties prop = new Properties();
prop.load(r);
List<String> paths = new ArrayList<>((Collection<String>) Collections.list(prop.propertyNames()));
Collections.sort(paths);
for (String k : paths) {
SystemUtils.fromCommonRelative(k);
String v = prop.getProperty(k, "").trim();
if (!v.isEmpty()) {
try {
PosixFilePermissions.fromString(v);
} catch (IllegalArgumentException ex) {
throwInvalidPermissions();
}
}
result.put(k, v);
}
return result;
}
@Override
public Map<String, String> loadPermissions() throws IOException {
return Collections.emptyMap();
}
@SuppressWarnings({"rawtypes", "unchecked"})
protected Map<String, String> parseSymlinks(Properties links) {
for (String key : new HashSet<>(links.stringPropertyNames())) {
Path p = SystemUtils.fromCommonRelative(key).normalize();
String prop = (String) links.remove(key);
links.setProperty(SystemUtils.toCommonPath(p), prop);
}
if (noVerifySymlinks) {
return new HashMap(links);
}
for (String s : Collections.list((Enumeration<String>) links.propertyNames())) {
String l = s;
Set<String> seen = new HashSet<>();
while (l != null) {
if (!seen.add(l)) {
throw feedback.failure("ERROR_CircularSymlink", null, l);
}
String target = links.getProperty(l);
Path linkPath = SystemUtils.fromCommonRelative(l);
SystemUtils.checkCommonRelative(linkPath, target);
Path targetPath = linkPath.resolveSibling(target).normalize();
String targetString = SystemUtils.toCommonPath(targetPath);
if (fileList.contains(targetString)) {
break;
}
String lt = links.getProperty(targetString);
if (lt == null) {
throw feedback.failure("ERROR_BrokenSymlink", null, target);
}
l = targetString;
}
}
return new HashMap(links);
}
@Override
public Map<String, String> loadSymlinks() throws IOException {
return Collections.emptyMap();
}
@Override
public void loadPaths() {
getComponentInfo();
}
@Override
public boolean isNoVerifySymlinks() {
return noVerifySymlinks;
}
@Override
public void setNoVerifySymlinks(boolean noVerifySymlinks) {
this.noVerifySymlinks = noVerifySymlinks;
}
@Override
public void close() throws IOException {
}
private void loadMessages(ComponentInfo nfo) {
String val = parseHeader(BundleConstants.BUNDLE_MESSAGE_POSTINST, null).getContents(null);
if (val != null) {
String text = val.replace("\\n", "\n").replace("\\\\", "\\");
nfo.setPostinstMessage(text);
}
}
protected void setLicensePath(String path) {
this.licensePath = path;
getComponentInfo().setLicensePath(licensePath);
}
protected void addFiles(List<String> files) {
fileList.addAll(files);
}
@Override
public ComponentInfo completeMetadata() throws IOException {
return getComponentInfo();
}
}