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

org.jruby.util.RegularFileResource Maven / Gradle / Ivy

package org.jruby.util;

import java.nio.channels.SeekableByteChannel;
import jnr.constants.platform.Errno;
import jnr.constants.platform.Fcntl;
import jnr.enxio.channels.NativeDeviceChannel;
import jnr.posix.FileStat;
import jnr.posix.POSIX;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;

import org.jruby.ext.fcntl.FcntlLibrary;
import org.jruby.RubyFile;
import org.jruby.platform.Platform;
import org.jruby.util.io.ModeFlags;
import org.jruby.util.io.PosixShim;

/**
 * Represents a "regular" file, backed by regular file system.
 */
class RegularFileResource implements FileResource {

    private final JRubyFile file;
    private final String filePath; // original (non-normalized) file-path
    private final POSIX posix;

    RegularFileResource(POSIX posix, JRubyFile file, String filePath) {
        this.file = file;
        this.filePath = filePath;
        this.posix = posix;
    }

    protected RegularFileResource(POSIX posix, String filename) {
        this.file = new JRubyFile(filename);
        this.filePath = filename;
        this.posix = posix;
    }

    @Override
    public String absolutePath() {
        return RubyFile.canonicalize(file.getAbsolutePath());
    }

    @Override
    public String canonicalPath() {
        // Seems like for Ruby absolute path implies resolving system links,
        // so canonicalization is in order.
        try {
            return file.getCanonicalPath();
        } catch (IOException ioError) {
            // I guess absolute path is next best thing?
            return file.getAbsolutePath();
        }
    }

    @Override
    public long length() {
        return file.length();
    }

    @Override
    public long lastModified() {
        return file.lastModified();
    }

    public FileTime lastModifiedTime() throws IOException {
        return FileTime.fromMillis(file.lastModified());
    }

    public FileTime lastAccessTime() throws IOException {
        return readAttributes().lastAccessTime();
    }

    public FileTime creationTime() throws IOException {
        return readAttributes().creationTime();
    }

    private transient BasicFileAttributes fileAttributes;

    private BasicFileAttributes readAttributes() throws IOException {
        if (fileAttributes != null) return fileAttributes;
        Path path = FileSystems.getDefault().getPath(file.getPath());
        return fileAttributes = Files.readAttributes(path, BasicFileAttributes.class);
    }

    @Override
    public boolean exists() {
        if (file.exists()) {
            String path = filePath;
            if (path.length() > 1 && path.charAt(path.length() - 1) == '/') {
                path = file.getPathDefault();
                if (path.length() > 0 && path.charAt(path.length() - 1) != '/' && !isDirectory()) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public boolean canExecute() {
        return file.canExecute();
    }

    public int errno() {
        return posix.errno();
    }

    @Override
    public boolean isFile() {
        return file.isFile();
    }

    @Override
    public boolean isDirectory() {
        return file.isDirectory();
    }

    @Override
    public boolean isSymLink() {
        try {
            // Note: We can't use file.exists() to check whether the symlink exists or not, because that method
            // returns false for existing but broken symlink. So, we try without the existence check, but in the
            // try-catch block.
            // MRI behavior: symlink? on broken symlink should return true.
            BasicFileAttributes attrs = Files.readAttributes(file.toPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);

            return attrs != null && attrs.isSymbolicLink();
        } catch (SecurityException|IOException|UnsupportedOperationException ex) {
            return false;
        }
    }

    @Override
    public boolean canRead() {
        return file.canRead();
    }

    @Override
    public boolean canWrite() {
        return file.canWrite();
    }

    @Override
    public String[] list() {
        String[] fileList = file.list();

        if (fileList == null) return null;

        // If we got some entries, then it's probably a directory and in Ruby all file
        // directories should have '.' and '..' entries
        String[] list = new String[fileList.length + 2];
        list[0] = ".";
        list[1] = "..";
        System.arraycopy(fileList, 0, list, 2, fileList.length);

        return list;
    }

    @Override
    public FileStat stat() {
        FileStat stat = posix.allocateStat();

        return posix.stat(file.getAbsolutePath(), stat) < 0 ? null : stat;
    }

    @Override
    public FileStat lstat() {
        FileStat stat = posix.allocateStat();

        return posix.lstat(file.getAbsolutePath(), stat) < 0 ? null : stat;
    }

    @Override
    public String toString() {
        return file.toString();
    }

    @Override
    public InputStream openInputStream() throws IOException {
        if (!exists()) {
            throw new ResourceException.NotFound(absolutePath());
        }
        if (isDirectory()) {
            throw new ResourceException.FileIsDirectory(absolutePath());
        }
        return new FileInputStream(file);
    }

    @Override
    public Channel openChannel(final int flags, int perm) throws IOException {
        final ModeFlags modeFlags = ModeFlags.createModeFlags(flags);
        if (posix.isNative() && !Platform.IS_WINDOWS) {
            int fd = posix.open(absolutePath(), modeFlags.getFlags(), perm);
            if (fd < 0) throwFromErrno(posix.errno());
            posix.fcntlInt(fd, Fcntl.F_SETFD, posix.fcntl(fd, Fcntl.F_GETFD) | FcntlLibrary.FD_CLOEXEC);
            return new NativeDeviceChannel(fd, true);
        }

        if (modeFlags.isCreate()) {
            boolean fileCreated;
            try {
                fileCreated = file.createNewFile();
            } catch (IOException ioe) {
                // See JRUBY-4380.
                // when the directory for the file doesn't exist.
                // Java in such cases just throws IOException.
                File parent = file.getParentFile();
                if (parent != null && parent != file && !parent.exists()) {
                    throw new ResourceException.NotFound(absolutePath());
                } else if (!file.canWrite()) {
                    throw new ResourceException.PermissionDenied(absolutePath());
                } else {
                    // for all other IO errors, we report it as general IO error
                    throw ioe;
                }
            }

            if (!fileCreated && modeFlags.isExclusive()) {
                throw new ResourceException.FileExists(absolutePath());
            }

            Channel channel = createChannel(modeFlags);

            // attempt to set the permissions, if we have been passed a POSIX instance,
            // perm is > 0, and only if the file was created in this call.
            if (fileCreated && posix != null) {
                perm = perm & ~PosixShim.umask(posix);

                if (perm > 0) posix.chmod(file.getPath(), perm);
            }

            return channel;
        }

        if (file.isDirectory()) {
            throw new ResourceException.FileIsDirectory(absolutePath());
        }

        if (!file.exists()) {
            throw new ResourceException.NotFound(absolutePath());
        }

        return createChannel(modeFlags);
    }

    private Channel createChannel(ModeFlags flags) throws IOException {
        SeekableByteChannel fileChannel;

        try{
            if (flags.isWritable() && !flags.isReadable()) {
                FileOutputStream fos = new FileOutputStream(file, flags.isAppendable());
                fileChannel = fos.getChannel();
            } else {
                RandomAccessFile raf = new RandomAccessFile(file, flags.toJavaModeString());
                fileChannel = raf.getChannel();

                // O_APPEND specifies that all writes will always be at the end of the open file
                // (even if we happened to have seek'd away from the end of the file o_O). RAF
                // does not have these semantics so we wrap it to support this unusual case.
                if (flags.isAppendable()) fileChannel = new AppendModeChannel((FileChannel) fileChannel);
            }
        }
        catch (FileNotFoundException ex) {
            throw mapFileNotFoundOnGetChannel(this, ex);
        }

        try {
            if (flags.isTruncate()) fileChannel.truncate(0);
        }
        catch (IOException ioe) {
            // ignore; it's a pipe or fifo that can't be truncated (we only care about illegal seek).
            if (!"Illegal seek".equals(ioe.getMessage())) throw ioe;
        }

        return fileChannel;
    }

    @Override
    public  T unwrap(Class type) {
        if (type == File.class || type == JRubyFile.class) return (T) file;
        throw new UnsupportedOperationException("unwrap: " + type.getName());
    }

    static IOException mapFileNotFoundOnGetChannel(final FileResource file, final FileNotFoundException ex) {
        // Java throws FileNotFoundException both if the file doesn't exist or there were
        // permission issues, but Ruby needs to disambiguate those two cases
        return file.exists() ?
                new ResourceException.PermissionDenied(file.absolutePath()) :
                new ResourceException.NotFound(file.absolutePath());
    }

    private void throwFromErrno(final int err) throws IOException {
        Errno errno = Errno.valueOf(err);
        switch (errno) {
            case EACCES:
                throw new ResourceException.PermissionDenied(absolutePath());
            case EEXIST:
                throw new ResourceException.FileExists(absolutePath());
            case EINVAL:
                throw new ResourceException.InvalidArguments(absolutePath());
            case ENOENT:
                throw new ResourceException.NotFound(absolutePath());
            case ELOOP:
                throw new ResourceException.TooManySymlinks(absolutePath());
            case EISDIR:
                throw new ResourceException.FileIsDirectory(absolutePath());
            case ENOTDIR:
                throw new ResourceException.FileIsNotDirectory(absolutePath());
            case EMFILE:
            default:
                throw new InternalIOException(errno.description());

        }
    }

    static class InternalIOException extends IOException {

        InternalIOException(final String message) {
            super(message);
        }

        @Override
        public Throwable fillInStackTrace() { return this; }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy