All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jruby.runtime.load.LibrarySearcher Maven / Gradle / Ivy

package org.jruby.runtime.load;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.regex.Matcher;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyDir;
import org.jruby.RubyFile;
import org.jruby.RubyString;
import org.jruby.ir.IRScope;
import org.jruby.platform.Platform;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.load.LoadService.SuffixType;
import org.jruby.util.ByteList;
import org.jruby.util.FileResource;
import org.jruby.util.JRubyFile;
import org.jruby.util.URLResource;
import org.jruby.util.cli.Options;
import org.jruby.util.collections.StringArraySet;
import org.jruby.util.func.TriFunction;

public class LibrarySearcher {
    public static final char EXTENSION_TYPE = 's';
    public static final char SOURCE_TYPE = 'r';
    public static final char UNKNOWN_TYPE = 'u';
    public static final char NOT_FOUND = 0;
    private final LoadService loadService;
    private final Ruby runtime;
    private final PathEntry cwdPathEntry;
    private final PathEntry classloaderPathEntry;
    private final PathEntry nullPathEntry;
    private final PathEntry homePathEntry;

    protected ExpandedLoadPath expandedLoadPath;

    protected RubyArray loadPath;
    protected RubyArray loadedFeaturesSnapshot;
    private final Map loadedFeaturesIndex = new ConcurrentHashMap<>(64);

    public LibrarySearcher(LoadService loadService) {
        Ruby runtime = loadService.runtime;

        this.runtime = runtime;
        this.loadPath = RubyArray.newArray(runtime);
        this.loadService = loadService;
        this.cwdPathEntry = new NormalPathEntry(runtime.newString("."));
        this.classloaderPathEntry = new NormalPathEntry(runtime.newString(URLResource.URI_CLASSLOADER));
        this.nullPathEntry = new NullPathEntry();
        this.homePathEntry = new HomePathEntry();
        this.loadedFeaturesSnapshot = runtime.newArray();
    }

    public List getExpandedLoadPath() {
        LibrarySearcher.ExpandedLoadPath expandedLoadPath = this.expandedLoadPath;

        if (expandedLoadPath == null || !expandedLoadPath.isCurrent()) {
            expandedLoadPath = this.expandedLoadPath = new ExpandedLoadPath(loadService.loadPath);
        }

        return expandedLoadPath.pathEntries;
    }

    @Deprecated
    public FoundLibrary findBySearchState(LoadService.SearchState state) {
        FoundLibrary[] lib = {null};
        char found = findLibraryForRequire(state.searchFile, lib);
        if (found != 0) {
            state.searchFile = lib[0].searchName;
            state.library = lib[0];
            state.setLoadName(lib[0].getLoadName());
        }
        return lib[0];
    }

    // MRI: search_required
    public synchronized char findLibraryForRequire(String file, FoundLibrary[] path) {
        // check loaded features
        FoundLibrary tmp;
        int ext, ftptr;
        char type, ft = 0;
        String[] loading = {null};

        path[0] = null;

        ext = file.lastIndexOf('.');
        if (ext != -1 && file.indexOf('/', ext) == -1) {
            if (isSourceExt(file)) {
                if (isFeature(file, ext, true, false, loading) != 0) {
                    if (loading[0] != null) path[0] = new FoundLibrary(file, loading[0], Suffix.RUBY.constructLibrary(file, loading[0], JRubyFile.createResourceAsFile(runtime, loading[0])));
                    return SOURCE_TYPE;
                }
                if ((tmp = findLibrary(file.substring(0, ext), SuffixType.Source)) != null) {
                    String tmpPath = tmp.loadName;
                    ext = tmpPath.lastIndexOf('.');
                    if (isFeature(tmpPath, ext, true, true, loading) == 0 || loading[0] != null)
                        path[0] = tmp;
                    return SOURCE_TYPE;
                }
                return 0;
            } else if (isLibraryExt(file)) {
                if (isFeature(file, ext, true, false, loading) != 0) {
                    if (loading[0] != null) path[0] = new FoundLibrary(file, loading[0], Suffix.JAR.constructLibrary(file, loading[0], JRubyFile.createResourceAsFile(runtime, loading[0])));
                    return EXTENSION_TYPE;
                }
                if ((tmp = findLibrary(file.substring(0, ext), SuffixType.Extension)) != null) {
                    ext = tmp.loadName.lastIndexOf('.');
                    if (isFeature(tmp.loadName, ext, false, true, loading) == 0 || loading[0] != null) {
                        path[0] = tmp;
                    }
                    return EXTENSION_TYPE;
                }
            }
        } else if ((ft = isFeature(file, -1, false, false, loading)) == SOURCE_TYPE) {
            if (loading[0] != null) path[0] = new FoundLibrary(file, loading[0], Suffix.RUBY.constructLibrary(file, loading[0], JRubyFile.createResourceAsFile(runtime, loading[0])));
            return SOURCE_TYPE;
        }

        tmp = findLibrary(file, SuffixType.Both);

        // CRuby: above call is rb_find_file_ext_safe and returns 1 for .rb found or >1 for some ext extension.
        if (tmp == null) {
            if (ft != 0) { // from extensionless search above
                if (loading[0] != null) path[0] = tmp;
                return ft;
            }
            return isFeature(file, -1, false, true, null);
        }

        // File was found, do a final check to see if it was loaded already
        boolean rb = true;
        if (tmp.loadName.endsWith(".jar")) {
            if (ft != 0) { // from extensionless search above
                if (loading[0] != null) path[0] = tmp;
                return ft;
            }
            rb = false;
        }
        ext = tmp.loadName.lastIndexOf('.');
        if (isFeature(tmp.loadName, ext, rb, true, loading) != 0 && loading[0] == null) {
            // Found in features but not in currently-loading features
        } else {
            // Not found in loaded features but did show up in currently-loading
            path[0] = tmp;
        }

        // If it's not a jar, it's Ruby source
        return tmp.loadName.endsWith(".jar") ? EXTENSION_TYPE : SOURCE_TYPE;
    }

    public LibrarySearcher.FoundLibrary findLibraryForLoad(String file) {
        if (file.endsWith(".rb")) {
            // source files need to check both .rb and .class so we use suffix logic
            return findLibrary(file.substring(0, file.length() - 3), SuffixType.Source);
        }

        // otherwise we try to load as-is
        FoundLibrary library = findResourceLibrary(file, f -> f, ResourceLibrary::create);

        if (library != null) {
            return library;
        }

        return findServiceLibrary(file);
    }

    public LibrarySearcher.FoundLibrary findLibrary(String baseName, SuffixType suffixType) {
        for (Suffix suffix : suffixType.getSuffixSet()) {
            FoundLibrary library = findResourceLibrary(baseName, suffix::forTarget, suffix.libraryFactory);

            if (library != null) {
                return library;
            }
        }

        return findServiceLibrary(baseName);
    }

    public static SuffixType getSuffixTypeForRequire(String[] fileHolder) {
        SuffixType suffixType;
        String file = fileHolder[0];

        // FIXME: Does this matter? We pass this through various path-normalizing calls elsewhere
        if (Platform.IS_WINDOWS) {
            file = file.replace('\\', '/');
        }

        int lastDot = file.lastIndexOf('.');

        if (lastDot != -1 && file.indexOf('/', lastDot) == -1) {
            if (isSourceExt(file)) {
                // source extensions
                suffixType = SuffixType.Source;

                // trim extension to try other options
                fileHolder[0] = file.substring(0, lastDot);
            } else if (isLibraryExt(file)) {

                // extension extensions
                suffixType = SuffixType.Extension;

                // trim extension to try other options
                fileHolder[0] = file.substring(0, lastDot);
            } else if (file.endsWith(".class")) {
                // For JRUBY-6731, treat require 'foo.class' as no other filename than 'foo.class'.
                suffixType = SuffixType.Neither;

            } else {
                // unknown extension, fall back to search with extensions
                suffixType = SuffixType.Both;
            }
        } else {
            // try all extensions
            suffixType = SuffixType.Both;
        }

        return suffixType;
    }

    public static SuffixType getSuffixTypeForLoad(final String[] fileHolder) {
        SuffixType suffixType;
        String file = fileHolder[0];

        int lastDot = file.lastIndexOf('.');

        if (lastDot != -1 && file.indexOf('/', lastDot) == -1) {
            // if a source extension is specified, try all source extensions
            Matcher matcher;
            if (isSourceExt(file)) {
                // source extensions
                suffixType = SuffixType.Source;

                // trim extension to try other options
                fileHolder[0] = file.substring(0, lastDot);
            } else {
                // unknown extension or .so/.jar, fall back to exact search
                suffixType = SuffixType.Neither;
            }
        } else {
            // try only literal search
            suffixType = SuffixType.Neither;
        }

        return suffixType;
    }

    public static boolean isSourceExt(String file) {
        return file.endsWith(".rb");
    }

    public static boolean isLibraryExt(String file) {
        return file.endsWith(".so") // Even if we don't support .so, some stdlib require .so directly.
                || file.endsWith(".o") // Matching MRI default extension search
                || file.endsWith(".jar");
    }

    private static class StringWrapper implements CharSequence {
        private String str;
        private int beg;
        private int len;
        private int hash;

        /**
         * Construct a wrapper with the given values.
         *
         * @param str
         * @param beg
         * @param len
         */
        StringWrapper(String str, int beg, int len) {
            this.str = str;
            this.beg = beg;
            this.len = len;
        }

        /**
         * Populate this wrapper with the given values.
         *
         * @param str
         * @param beg
         * @param len
         */
        void rewrap(String str, int beg, int len) {
            this.str = str;
            this.beg = beg;
            this.len = len;
            this.hash = 0;
        }

        /**
         * Clear this wrapper.
         */
        void clear() {
            str = null;
            beg = 0;
            len = 0;
            hash = 0;
        }

        StringWrapper dup() {
            return new StringWrapper(str, beg, len);
        }

        @Override
        public int length() {
            return len;
        }

        @Override
        public char charAt(int index) {
            return str.charAt(beg + index);
        }

        @Override
        public CharSequence subSequence(int start, int end) {
            return new StringWrapper(str, beg + start, end - start);
        }

        @Override
        public String toString() {
            return str.substring(beg, beg + len);
        }

        @Override
        public boolean equals(Object other) {
            String otherStr;
            String str;
            int otherBeg;
            int otherLen;

            int beg = this.beg;
            int len = this.len;

            if (other instanceof StringWrapper) {
                StringWrapper otherWrapper = (StringWrapper) other;

                otherLen = otherWrapper.len;

                if (len != otherLen) return false;

                otherBeg = otherWrapper.beg;
                str = this.str;
                otherStr = otherWrapper.str;
            } else if (other instanceof String) {
                otherStr = (String) other;

                otherLen = otherStr.length();

                if (len != otherLen) return false;

                otherBeg = 0;
                str = this.str;
            } else {
                return false;
            }

            if (str == otherStr && beg == otherBeg) return true;

            return str.regionMatches(beg, otherStr, otherBeg, otherLen);
        }

        @Override
        public int hashCode() {
            int h = hash;
            int len = this.len;
            if (h == 0 && len > 0) {
                String str = this.str;
                int beg = this.beg;
                for (int i = 0; i < len; i++) {
                    h = 31 * h + str.charAt(beg + i);
                }
                hash = h;
            }
            return h;
        }
    }

    public synchronized boolean featureAlreadyLoaded(String feature, String[] loading) {
        int ext = feature.lastIndexOf('.');

        if (feature.charAt(0) == '.' &&
                (feature.charAt(1) == '/' || feature.regionMatches(1, "./", 0, 2))) {
            feature = RubyFile.expand_path(runtime.getCurrentContext(), runtime.getFile(), runtime.newString(feature)).asJavaString();
        }
        if (ext != -1 && feature.indexOf('/', ext) == -1) {
            if (LibrarySearcher.isSourceExt(feature)) {
                if (isFeature(feature, ext, true, false, loading) != 0) return true;
                return false;
            }
            else if (LibrarySearcher.isLibraryExt(feature)) {
                if (isFeature(feature, ext, false, false, loading) != 0) return true;
                return false;
            }
        }
        if (isFeature(feature, -1, true, false, loading) != 0)
            return true;
        return false;
    }

    protected synchronized void provideFeature(RubyString name) {
        StringArraySet loadedFeatures = this.loadService.loadedFeatures;

        if (loadedFeatures.isFrozen()) {
            throw runtime.newRuntimeError("$LOADED_FEATURES is frozen; cannot append feature");
        }

        name.setFrozen(true);

        loadedFeatures.append(name);

        snapshotLoadedFeatures();

        addFeatureToIndex(name.toString(), name);
    }

    protected synchronized RubyArray snapshotLoadedFeatures() {
        return (RubyArray) loadedFeaturesSnapshot.replace(this.loadService.loadedFeatures);
    }

    protected synchronized void addFeatureToIndex(String name, IRubyObject featurePath) {
        int featureEnd = name.length();

        int ext, p;

        for (ext = featureEnd - 1; ext > 0; ext--)
            if (name.charAt(ext) == '.' || name.charAt(ext) == '/') break;

        if (name.charAt(ext) != '.') ext = -1;

        /* Now `ext` points to the only string matching %r{^\.[^./]*$} that is
           at the end of `feature`, or is NULL if there is no such string. */

        p = ext != -1 ? ext : featureEnd;
        while (true) {
            p--;

            // Walk back to nearest '/'
            while (p >= 0 && name.charAt(p) != '/') p--;

            if (p < 0) break;

            // Add partial feature to index
            addSingleFeatureToIndex(name, p + 1, featureEnd, featurePath);

            // Add partial feature without extension, if appropriate
            if (ext != -1) {
                addSingleFeatureToIndex(name, p + 1, ext, featurePath);
            }
        }

        // Add full feature to index
        addSingleFeatureToIndex(name, 0, name.length(), featurePath);

        // Add version without extension, if appropriate
        if (ext != -1) {
            addSingleFeatureToIndex(name, 0, ext, featurePath);
        }
    }

    class Feature {
        Feature(StringWrapper key, IRubyObject featurePath) {
            this.key = key;
            this.featurePaths = new ArrayList<>();

            featurePaths.add(featurePath);
        }

        synchronized void addFeaturePath(IRubyObject offset) {
            featurePaths.add(offset);
        }

        public synchronized void clear() {
            featurePaths.clear();
        }

        public synchronized char matches(String feature, Suffix suffix, int len, boolean rb, boolean expanded, boolean suffixGiven) {
            List featurePaths = this.featurePaths;
            for (int i = 0; i < featurePaths.size(); i++) {
                IRubyObject featurePath = featurePaths.get(i);

                RubyString loadedFeaturePath = featurePath.convertToString();

                if (loadedFeaturePath.length() < len) continue;

                String featureString = loadedFeaturePath.asJavaString();

                int expandedPathLength = 0;

                if (!featureString.regionMatches(0, feature, 0, len)) {
                    if (expanded) continue; // already given expanded path

                    List loadPath = getExpandedLoadPath();
                    String withPath = loadedFeatureWithPath(featureString, feature, suffix, loadPath);

                    if (withPath == null) continue; // no match with expanded path, try next

                    expanded = true;
                    expandedPathLength = withPath.length() + 1;
                }

                int e = expandedPathLength + len;
                if (e == featureString.length()) {
                    if (suffixGiven) continue;
                    return UNKNOWN_TYPE;
                }
                if (featureString.charAt(e) != '.') continue;
                if ((!rb || !suffixGiven) && isLibraryExt(featureString)) {
                    return EXTENSION_TYPE;
                }
                if ((rb || !suffixGiven) && isSourceExt(featureString)) {
                    return SOURCE_TYPE;
                }
            }

            return 0;
        }

        final StringWrapper key;
        private final List featurePaths;
    }

    private final ThreadLocal keyWrapper = ThreadLocal.withInitial(() -> new StringWrapper(null, 0, 0));
    private final ThreadLocal rangeWrapper = ThreadLocal.withInitial(() -> new StringWrapper(null, 0, 0));

    protected synchronized void addSingleFeatureToIndex(String key, int beg, int end, IRubyObject featurePath) {
        Map featuresIndex = this.loadedFeaturesIndex;

        withRangeWrapper(key, beg, end - beg, (wrapper) -> {
            Feature thisFeature = featuresIndex.get(wrapper);

            if (thisFeature == null) {
                StringWrapper clone = wrapper.dup();
                featuresIndex.put(clone, new Feature(clone, featurePath));
            } else {
                thisFeature.addFeaturePath(featurePath);
            }

            return thisFeature;
        });
    }

    private synchronized void releaseWrapper(StringWrapper wrapper) {
        wrapper.clear();
    }

    private synchronized StringWrapper keyWrapper(String key) {
        StringWrapper wrapper = keyWrapper.get();
        wrapper.rewrap(key, 0, key.length());
        return wrapper;
    }

    private synchronized StringWrapper rangeWrapper(String key, int beg, int len) {
        StringWrapper wrapper = rangeWrapper.get();
        wrapper.rewrap(key, beg, len);
        return wrapper;
    }

    private synchronized  R withKeyWrapper(String key, Function body) {
        StringWrapper wrapper = keyWrapper(key);
        R result = body.apply(wrapper);
        releaseWrapper(wrapper);
        return result;
    }

    private synchronized  R withRangeWrapper(String key, int beg, int len, Function body) {
        StringWrapper wrapper = rangeWrapper(key, beg, len);
        R result = body.apply(wrapper);
        releaseWrapper(wrapper);
        return result;
    }

    protected char isFeature(String feature, int ext, boolean rb, boolean expanded, String[] fn) {
        List loadPath = null;

        final LibrarySearcher.Suffix suffix;

        if (fn != null) fn[0] = null;

        boolean suffixGiven = ext != -1;

        final int len;
        if (suffixGiven) {
            len = ext;
            suffix = rb ? LibrarySearcher.Suffix.RUBY : LibrarySearcher.Suffix.JAR;
        } else {
            len = feature.length();
            suffix = null;
        }

        /*
        This caching logic assumes an index mapping all loaded paths and subpaths to potential loaded features. Each
        feature is added to the cache as a series of subpaths pointing at the original loaded feature entry. This
        allows subsequent requires of that path or similar subpaths (as in require_relative) to quickly check all
        previously added features for a match.

        The matches are calculated by taking the given feature path combined with load path entries and file suffixes
        and attempting to match it against any loaded features this path has been associated with.
         */
        Feature matchingFeature = getLoadedFeature(feature);
        if (matchingFeature != null) {
            char matches = matchingFeature.matches(feature, suffix, len, rb, expanded, suffixGiven);

            if (matches != 0) return matches;
        }

        // Check load locks to see if another thread is currently loading this file
        Map loadingTable = this.loadService.requireLocks.pool;
        if (!expanded) {
            loadPath = lazyLoadPath(loadPath);
            for (Map.Entry entry : loadingTable.entrySet()) {
                if (loadedFeatureWithPath(entry.getKey(), feature, suffix, loadPath) != null) {
                    return setLoadingAndReturn(feature, fn, suffixGiven, entry.getKey());
                }
            }
        }

        if (loadingTable.containsKey(feature)) {
            // FIXME: use key from the actual table?
            return setLoadingAndReturn(feature, fn, suffixGiven, feature);
        }

        if (suffixGiven && ext == feature.length()) return 0;
        String baseName = feature.substring(0, len);
        for (LibrarySearcher.Suffix suffix2 : Suffix.ALL_ARY) {
            String withExt = suffix2.forTarget(baseName);
            if (loadingTable.containsKey(withExt)) {
                if (fn != null) fn[0] = withExt;
                return suffix2 != LibrarySearcher.Suffix.RUBY ? EXTENSION_TYPE : SOURCE_TYPE;
            }
        }

        return NOT_FOUND;
    }

    private char setLoadingAndReturn(String feature, String[] fn, boolean suffixGiven, String key) {
        if (fn != null) fn[0] = key;
        if (!suffixGiven) return UNKNOWN_TYPE;
        return !isSourceExt(feature) ? EXTENSION_TYPE : SOURCE_TYPE;
    }

    private List lazyLoadPath(List loadPath) {
        return (loadPath == null) ? getExpandedLoadPath() : loadPath;
    }

    synchronized Map getLoadedFeaturesIndex() {
        StringArraySet loadedFeatures = this.loadService.loadedFeatures;

        // Compare to see if the snapshot still matches actual.
        if (!loadedFeaturesSnapshot.isSharedJavaArray(loadedFeatures)) {
            loadedFeaturesIndex.clear();

            Ruby runtime = this.runtime;

            // defensive copy for iteration; if original is modified we snapshot again below
            RubyArray features = snapshotLoadedFeatures();
            boolean modified = false;

            for (int i = 0; i < features.size(); i++) {
                IRubyObject entry = features.eltOk(i);
                RubyString asStr = runtime.freezeAndDedupString(entry.convertToString());
                if (asStr != entry) {
                    modified = true;
                    loadedFeatures.eltSetOk(i, asStr);
                }
                addFeatureToIndex(asStr.toString(), asStr);
            }

            if (modified) snapshotLoadedFeatures();
        }

        return loadedFeaturesIndex;
    }

    Feature getLoadedFeature(String feature) {
        return withKeyWrapper(feature, (wrapper) -> getLoadedFeaturesIndex().get(wrapper));
    }

    /* This searches `load_path` for a value such that
         name == "#{load_path[i]}/#{feature}"
       if `feature` is a suffix of `name`, or otherwise
         name == "#{load_path[i]}/#{feature}#{ext}"
       for an acceptable string `ext`.  It returns
       `load_path[i].to_str` if found, else 0.

       If type is 's', then `ext` is acceptable only if IS_DLEXT(ext);
       if 'r', then only if IS_RBEXT(ext); otherwise `ext` may be absent
       or have any value matching `%r{^\.[^./]*$}`.
    */
    static String loadedFeatureWithPath(String name, String feature,
                                 LibrarySearcher.Suffix suffix, List loadPath) {
        final int nameLength = name.length();
        final int featureLength = feature.length();

        int plen;

        if (nameLength <= featureLength) return null;
        if (feature.indexOf('.') != -1 && name.endsWith(feature)) {
            plen = nameLength - featureLength;
        }
        else {
            int e;
            for (e = nameLength - 1; e >= 0 && name.charAt(e) != '.' && name.charAt(e) != '/'; --e);
            if (name.charAt(e) != '.' ||
                    e < featureLength ||
                    !name.regionMatches(e - featureLength, feature, 0, featureLength))
                return null;
            plen = e - featureLength;
        }
        if (plen > 0 && name.charAt(plen-1) != '/') {
            return null;
        }
        if (
                (suffix == LibrarySearcher.Suffix.JAR && !LibrarySearcher.isLibraryExt(name)) ||
                        (suffix == LibrarySearcher.Suffix.RUBY && !LibrarySearcher.isSourceExt(name))) {

            return null;
        }

        /* Now name == "#{prefix}/#{feature}#{ext}" where ext is acceptable
           (possibly empty) and prefix is some string of length plen. */

        if (plen > 0) --plen;	/* exclude '.' */
        for (int i = 0; i < loadPath.size(); ++i) {
            LibrarySearcher.PathEntry pathEntry = loadPath.get(i);
            String path = pathEntry.path();

            int n = path.length();

            if (n != plen) continue;
            if (n > 0 && !name.startsWith(path)) {
                continue;
            }

            return path;
        }
        return null;
    }

    private FoundLibrary findServiceLibrary(String name) {
        DebugLog.JarExtension.logTry(name);
        Library extensionLibrary = ClassExtensionLibrary.tryFind(runtime, name);
        if (extensionLibrary != null) {
            DebugLog.JarExtension.logFound(name);
            return new FoundLibrary(name, name, extensionLibrary);
        } else {
            return null;
        }
    }

    private FoundLibrary findResourceLibrary(String baseName, FilenameFactory pathMaker, LibraryFactory libraryMaker) {
        if (baseName.startsWith("./") || baseName.startsWith("../") || isAbsolute(baseName)) {
            // Path should be canonicalized in the findFileResource
            return nullPathEntry.findFile(baseName, pathMaker, libraryMaker);
        }

        if (baseName.startsWith("~/")) {
            return homePathEntry.findFile(baseName.substring(2), pathMaker, libraryMaker);
        }

        // search the $LOAD_PATH
        for (PathEntry loadPathEntry : getExpandedLoadPath()) {
            FoundLibrary library = loadPathEntry.findFile(baseName, pathMaker, libraryMaker);
            if (library != null) return library;
        }

        // inside a classloader the path "." is the place where to find the jruby kernel
        if (!runtime.getCurrentDirectory().startsWith(URLResource.URI_CLASSLOADER)) {

            // ruby does not load a relative path unless the current working directory is in $LOAD_PATH
            FoundLibrary library = cwdPathEntry.findFile(baseName, pathMaker, libraryMaker);

            // we did not find the file on the $LOAD_PATH but in current directory so we need to treat it
            // as not found (the classloader search below will find it otherwise)
            if (library != null) return null;
        }

        // load the jruby kernel and all resource added to $CLASSPATH
        return classloaderPathEntry.findFile(baseName, pathMaker, libraryMaker);
    }

    private static boolean isAbsolute(String path) {
        return isURI(path) || new File(path).isAbsolute();
    }

    private static boolean isURI(String path) {
        // jar: prefix doesn't mean anything anymore, but we might still encounter it
        if (path.startsWith("jar:")) {
            path = path.substring(4);
        }

        if (path.startsWith("file:")) {
            // We treat any paths with a file schema as absolute, because apparently some tests
            // explicitely depend on such behavior (test/test_load.rb). On other hand, maybe it's
            // not too bad, since otherwise joining LOAD_PATH logic would be more complicated if
            // it'd have to worry about schema.
            return true;
        }
        if (path.startsWith("uri:")) {
            // uri: are absolute
            return true;
        }
        if (path.startsWith("classpath:")) {
            // classpath URLS are always absolute
            return true;
        }

        return false;
    }

    enum Suffix {
        RUBY(".rb", ResourceLibrary::new),
        CLASS(".class", ClassResourceLibrary::new),
        JAR(".jar", JarResourceLibrary::new);

        // If .class is enabled, we still search for .rb first
        static final EnumSet SOURCES =
                Options.AOT_LOADCLASSES.load() ?
                        EnumSet.of(RUBY, CLASS) :
                        EnumSet.of(RUBY);
        static final EnumSet EXTENSIONS = EnumSet.of(JAR);
        static final EnumSet ALL =
                Options.AOT_LOADCLASSES.load() ?
                        EnumSet.of(RUBY, CLASS, JAR) :
                        EnumSet.of(RUBY, JAR);
        static final Suffix[] ALL_ARY = ALL.stream().toArray(i -> new Suffix[i]);

        private final String extension;
        private final byte[] extensionBytes;
        private final LibraryFactory libraryFactory;

        Suffix(String extension, LibraryFactory libraryFactory) {
            this.extension = extension;
            this.extensionBytes = extension.getBytes();
            this.libraryFactory = libraryFactory;
        }

        public Library constructLibrary(String target, String name, FileResource fullPath) {
            return libraryFactory.apply(target, name, fullPath);
        }

        public String forTarget(String targetName) {
            return targetName + extension;
        }

        public ByteList forTarget(ByteList targetName) {
            ByteList dup = targetName.shallowDup();
            dup.append(extensionBytes);
            return dup;
        }

        public static Suffix forString(String withSuffix) {
            int last = withSuffix.lastIndexOf('.');

            if (last > -1) {
                switch (withSuffix.substring(last)) {
                    case ".rb":
                        return RUBY;
                    case ".jar":
                        return JAR;
                    case ".class":
                        return CLASS;
                }
            }

            throw new RuntimeException("invalid suffix in LoadService (missing '.'?): " + withSuffix);
        }
    }

    public static class FoundLibrary implements Library {
        private final Library delegate;
        private final String searchName;
        private final String loadName;

        public FoundLibrary(String searchName, String loadName, Library delegate) {
            this.searchName = searchName;
            this.loadName = loadName;
            this.delegate = delegate;
        }

        @Override
        public void load(Ruby runtime, boolean wrap) throws IOException {
            delegate.load(runtime, wrap);
        }

        public String getLoadName() {
            return loadName;
        }

        public String getSearchName() {
            return searchName;
        }
    }

    static class ResourceLibrary implements Library {
        public static ResourceLibrary create(String searchName, String scriptName, FileResource resource) {
            String location = resource.absolutePath();

            if (location.endsWith(".class")) return new ClassResourceLibrary(searchName, scriptName, resource);
            if (location.endsWith(".jar")) return new JarResourceLibrary(searchName, scriptName, resource);

            return new ResourceLibrary(searchName, scriptName, resource); // just .rb?
        }

        protected final String searchName;
        protected final String scriptName;
        protected final FileResource resource;
        protected final String location;

        public ResourceLibrary(String searchName, String scriptName, FileResource resource) {
            this.searchName = searchName;
            this.scriptName = scriptName;
            this.location = resource.absolutePath();
            this.resource = resource;
        }

        @Override
        public void load(Ruby runtime, boolean wrap) {
            // Fully buffers file, so does not need to be closed
            LoadServiceResourceInputStream ris = prepareInputStream(runtime);

            if (runtime.getInstanceConfig().getCompileMode().shouldPrecompileAll()) {
                runtime.compileAndLoadFile(scriptName, ris, wrap);
            } else {
                runtime.loadFile(scriptName, ris, wrap);
            }
        }

        private LoadServiceResourceInputStream prepareInputStream(Ruby runtime) {
            try (InputStream is = resource.inputStream()){
                return new LoadServiceResourceInputStream(is);
            } catch (IOException ioe) {
                throw runtime.newLoadError("failure to load file: " + ioe.getLocalizedMessage(), searchName);
            }
        }
    }

    static class ClassResourceLibrary extends ResourceLibrary {
        public ClassResourceLibrary(String searchName, String scriptName, FileResource resource) {
            super(searchName, scriptName, resource);
        }

        @Override
        public void load(Ruby runtime, boolean wrap) {
            try (InputStream ris = resource.inputStream()) {

                InputStream is = new BufferedInputStream(ris, 32768);
                IRScope script = CompiledScriptLoader.loadScriptFromFile(runtime, is, null, scriptName, false);

                // Depending on the side-effect of the load, which loads the class but does not turn it into a script.
                // I don't like it, but until we restructure the code a bit more, we'll need to quietly let it by here.
                if (script == null) return;

                script.setFileName(scriptName);
                runtime.loadScope(script, wrap);
            } catch(IOException e) {
                throw runtime.newLoadError("no such file to load -- " + searchName, searchName);
            }
        }
    }

    static class JarResourceLibrary extends ResourceLibrary {
        public JarResourceLibrary(String searchName, String scriptName, FileResource resource) {
            super(searchName, scriptName, resource);
        }

        @Override
        public void load(Ruby runtime, boolean wrap) {
            try {
                URL url;
                if (location.startsWith(URLResource.URI)) {
                    url = URLResource.getResourceURL(runtime, location);
                } else {
                    // convert file urls with !/ into jar urls so the classloader
                    // can handle them via protocol handler
                    File f = new File(location);
                    if (f.exists() || location.contains( "!")){
                        url = f.toURI().toURL();
                        if (location.contains( "!")) {
                            url = new URL( "jar:" + url );
                        }
                    } else {
                        url = new URL(location);
                    }
                }
                runtime.getJRubyClassLoader().addURL(url);
            }
            catch (MalformedURLException badUrl) {
                throw runtime.newIOErrorFromException(badUrl);
            }

            // If an associated Service library exists, load it as well
            ClassExtensionLibrary serviceExtension = ClassExtensionLibrary.tryFind(runtime, searchName);
            if (serviceExtension != null) {
                serviceExtension.load(runtime, wrap);
            }
        }
    }

    class ExpandedLoadPath {
        final RubyArray loadPath;
        final RubyArray loadPathSnapshot;
        final List pathEntries;

        ExpandedLoadPath(RubyArray loadPath) {
            this.loadPath = loadPath;

            RubyArray loadPathSnapshot = loadPath.aryDup();
            List pathEntries = new ArrayList<>(loadPathSnapshot.size());

            for (int i = 0; i < loadPathSnapshot.size(); i++) {
                IRubyObject path = loadPathSnapshot.eltOk(i);

                pathEntries.add(new NormalPathEntry(path));
            }
            this.loadPathSnapshot = loadPathSnapshot;
            this.pathEntries = pathEntries;
        }

        boolean isCurrent() {
            return loadPathSnapshot.isSharedJavaArray(loadPath);
        }
    }

    interface LibraryFactory extends TriFunction {}
    interface FilenameFactory extends Function {}

    abstract class PathEntry {
        protected FoundLibrary findFile(String target, FilenameFactory pathMaker, LibraryFactory libraryMaker) {
            Ruby runtime = LibrarySearcher.this.runtime;

            FileResource fullPath = fullPath(target, pathMaker);

            // Can't determine a full path for this entry, return no result
            if (fullPath == null) {
                return null;
            }

            if (fullPath.exists()) {
                String canonicalPath = fullPath.canonicalPath();
                String absolutePath = fullPath.absolutePath();
                if (!absolutePath.equals(canonicalPath)) {
                    FileResource expandedResource = JRubyFile.createResourceAsFile(runtime, canonicalPath);

                    if (expandedResource.exists()){
                        String expandedAbsolute = expandedResource.absolutePath();

                        DebugLog.Resource.logFound(fullPath);

                        return new FoundLibrary(target, expandedAbsolute, libraryMaker.apply(target, expandedAbsolute, fullPath));
                    }
                }

                DebugLog.Resource.logFound(fullPath);

                String resolvedName = absolutePath;

                return new FoundLibrary(target, resolvedName, libraryMaker.apply(target, resolvedName, fullPath));
            }

            return null;
        }

        protected abstract String path();

        protected abstract FileResource fullPath(String searchName, Function pathMaker);
    }

    class NormalPathEntry extends PathEntry {
        final IRubyObject path;
        final boolean cacheExpanded;
        FileResource expanded;

        NormalPathEntry(IRubyObject path) {
            this.path = path;
            this.cacheExpanded = isCachable(runtime, path);
        }

        protected String path() {
            return expandPathCached().path();
        }

        protected FileResource fullPath(String searchFile, Function pathMaker) {
            FileResource loadPath = expandPathCached();

            String fullPath = loadPath.path() + "/"
                    + pathMaker.apply(searchFile);

            DebugLog.Resource.logTry(fullPath);

            return JRubyFile.createResourceAsFile(runtime, fullPath);
        }

        private FileResource expandPathCached() {
            if (cacheExpanded) {
                FileResource expanded = this.expanded;
                if (expanded != null) return expanded;

                expanded = this.expanded = expandPath();

                return expanded;
            }

            FileResource expanded = expandPath();

            return expanded;
        }

        private FileResource expandPath() {
            FileResource resource = JRubyFile.createResourceAsFile(runtime, Helpers.javaStringFromPath(runtime, path));

            return JRubyFile.createResourceAsFile(runtime, resource.canonicalPath());
        }

        private boolean isCachable(Ruby runtime, IRubyObject path) {
            if (!(path instanceof RubyString)) return false;

            String pathAsString = path.asJavaString();

            if (pathAsString.length() == 0) return false;

            if (isURI(pathAsString)) return false;

            if (!new File(pathAsString).isAbsolute()) return false;

            FileResource resource = JRubyFile.createResourceAsFile(runtime, pathAsString);

            return !resource.isFile();
        }
    }

    class HomePathEntry extends PathEntry {
        protected String path() {
            return resolveHome();
        }

        protected FileResource fullPath(String searchFile, Function pathMaker) {
            String fullPath = resolveHome();

            if (fullPath == null) return null;

            DebugLog.Resource.logTry(fullPath);

            return JRubyFile.createResourceAsFile(runtime, fullPath + "/" + pathMaker.apply(searchFile));
        }

        private String resolveHome() {
            Optional home = RubyDir.getHomeFromEnv(runtime);

            // FIXME: Ick. See #5661
            if (!home.isPresent()) return null;

            String fullPath = home.get();
            return fullPath;
        }
    }

    class NullPathEntry extends PathEntry {
        protected String path() {
            return "";
        }

        protected FileResource fullPath(String searchFile, Function pathMaker) {
            return JRubyFile.createResourceAsFile(runtime, pathMaker.apply(searchFile));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy