/*
 * Copyright 2007-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package sun.nio.fs;

import java.security.AccessController;
import java.io.*;
import java.net.URI;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.*;
import sun.security.util.SecurityConstants;

import org.classpath.icedtea.java.io.FilePermission;

import org.classpath.icedtea.java.nio.channels.SeekableByteChannel;

import org.classpath.icedtea.java.nio.file.AccessMode;
import org.classpath.icedtea.java.nio.file.CopyOption;
import org.classpath.icedtea.java.nio.file.DirectoryNotEmptyException;
import org.classpath.icedtea.java.nio.file.DirectoryStream;
import org.classpath.icedtea.java.nio.file.FileRef;
import org.classpath.icedtea.java.nio.file.FileStore;
import org.classpath.icedtea.java.nio.file.FileSystemException;
import org.classpath.icedtea.java.nio.file.InvalidPathException;
import org.classpath.icedtea.java.nio.file.LinkOption;
import org.classpath.icedtea.java.nio.file.LinkPermission;
import org.classpath.icedtea.java.nio.file.NotDirectoryException;
import org.classpath.icedtea.java.nio.file.NotLinkException;
import org.classpath.icedtea.java.nio.file.OpenOption;
import org.classpath.icedtea.java.nio.file.Path;
import org.classpath.icedtea.java.nio.file.ProviderMismatchException;
import org.classpath.icedtea.java.nio.file.StandardOpenOption;
import org.classpath.icedtea.java.nio.file.WatchEvent;
import org.classpath.icedtea.java.nio.file.WatchKey;
import org.classpath.icedtea.java.nio.file.WatchService;

import org.classpath.icedtea.java.nio.file.attribute.FileAttribute;
import org.classpath.icedtea.java.nio.file.attribute.FileAttributeView;

import org.classpath.icedtea.java.nio.file.spi.AbstractPath;

import static sun.nio.fs.UnixNativeDispatcher.*;
import static sun.nio.fs.UnixConstants.*;

/**
 * Solaris/Linux implementation of java.nio.file.Path
 */

class UnixPath
    extends AbstractPath
{
    // FIXME - eliminate this reference to reduce space
    private final UnixFileSystem fs;

    // internal representation
    private final byte[] path;

    // String representation (created lazily)
    private volatile String stringValue;

    // cached hashcode (created lazily, no need to be volatile)
    private int hash;

    // array of offsets of elements in path (created lazily)
    private volatile int[] offsets;

    // file permissions (created lazily)
    private volatile FilePermission[] perms;

    UnixPath(UnixFileSystem fs, byte[] path) {
        this.fs = fs;
        this.path = path;
    }

    UnixPath(UnixFileSystem fs, String input) {
        // removes redundant slashes and checks for invalid characters
        this(fs, normalizeAndCheck(input).getBytes());
    }

    // package-private
    // removes redundant slashes and check input for invalid characters
    static String normalizeAndCheck(String input) {
        int n = input.length();
        if (n == 0)
            throw new InvalidPathException(input, "Path is empty");
        char prevChar = 0;
        for (int i=0; i < n; i++) {
            char c = input.charAt(i);
            if (c == '\u0000')
                throw new InvalidPathException(input, "Nul character not allowed");
            if ((c == '/') && (prevChar == '/'))
                return normalize(input, n, i - 1);
            prevChar = c;
        }
        if (prevChar == '/')
            return normalize(input, n, n - 1);
        return input;
    }

    private static String normalize(String input, int len, int off) {
        if (len == 0)
            return input;
        int n = len;
        while ((n > 0) && (input.charAt(n - 1) == '/')) n--;
        if (n == 0)
            return "/";
        StringBuilder sb = new StringBuilder(input.length());
        if (off > 0)
            sb.append(input.substring(0, off));
        char prevChar = 0;
        for (int i=off; i < n; i++) {
            char c = input.charAt(i);
            if ((c == '/') && (prevChar == '/'))
                continue;
            sb.append(c);
            prevChar = c;
        }
        return sb.toString();
    }

    // package-private
    byte[] asByteArray() {
        return path;
    }

    // use this path when making system/library calls
    byte[] getByteArrayForSysCalls() {
        // resolve against default directory if required (chdir allowed or
        // file system default directory is not working directory)
        if (getFileSystem().needToResolveAgainstDefaultDirectory()) {
            return resolve(getFileSystem().defaultDirectory(), path);
        } else {
            return path;
        }
    }

    // use this message when throwing exceptions
    String getPathForExecptionMessage() {
        return toString();
    }

    // use this path for permission checks
    String getPathForPermissionCheck() {
        if (getFileSystem().needToResolveAgainstDefaultDirectory()) {
            return new String(getByteArrayForSysCalls());
        } else {
            return toString();
        }
    }

    // Checks that the given file is a UnixPath
    private UnixPath checkPath(FileRef obj) {
        if (obj == null)
            throw new NullPointerException();
        if (!(obj instanceof UnixPath))
            throw new ProviderMismatchException();
        return (UnixPath)obj;
    }

    // create offset list if not already created
    private void initOffsets() {
        if (offsets == null) {
            int count, index;

            // count names
            count = 0;
            index = 0;
            while (index < path.length) {
                byte c = path[index++];
                if (c != '/') {
                    count++;
                    while (index < path.length && path[index] != '/')
                        index++;
                }
            }

            // populate offsets
            int[] result = new int[count];
            count = 0;
            index = 0;
            while (index < path.length) {
                byte c = path[index];
                if (c == '/') {
                    index++;
                } else {
                    result[count++] = index++;
                    while (index < path.length && path[index] != '/')
                        index++;
                }
            }
            synchronized (this) {
                if (offsets == null)
                    offsets = result;
            }
        }
    }


    public UnixFileSystem getFileSystem() {
        return fs;
    }


    public UnixPath getRoot() {
        if (path[0] == '/') {
            return getFileSystem().rootDirectory();
        } else {
            return null;
        }
    }


    public UnixPath getName() {
        initOffsets();

        int count = offsets.length;
        if (count == 0)
            return null;  // no elements so no name

        if (count == 1 && path[0] != '/')
            return this;

        int lastOffset = offsets[count-1];
        int len = path.length - lastOffset;
        byte[] result = new byte[len];
        System.arraycopy(path, lastOffset, result, 0, len);
        return new UnixPath(getFileSystem(), result);
    }


    public UnixPath getParent() {
        initOffsets();

        int count = offsets.length;
        if (count == 0) {
            // no elements so no parent
            return null;
        }
        int len = offsets[count-1] - 1;
        if (len <= 0) {
            // parent is root only (may be null)
            return getRoot();
        }
        byte[] result = new byte[len];
        System.arraycopy(path, 0, result, 0, len);
        return new UnixPath(getFileSystem(), result);
    }


    public int getNameCount() {
        initOffsets();
        return offsets.length;
    }


    public UnixPath getName(int index) {
        initOffsets();
        if (index < 0)
            throw new IllegalArgumentException();
        if (index >= offsets.length)
            throw new IllegalArgumentException();

        int begin = offsets[index];
        int len;
        if (index == (offsets.length-1)) {
            len = path.length - begin;
        } else {
            len = offsets[index+1] - begin - 1;
        }

        // construct result
        byte[] result = new byte[len];
        System.arraycopy(path, begin, result, 0, len);
        return new UnixPath(getFileSystem(), result);
    }


    public UnixPath subpath(int beginIndex, int endIndex) {
        initOffsets();

        if (beginIndex < 0)
            throw new IllegalArgumentException();
        if (beginIndex >= offsets.length)
            throw new IllegalArgumentException();
        if (endIndex > offsets.length)
            throw new IllegalArgumentException();
        if (beginIndex >= endIndex) {
            throw new IllegalArgumentException();
        }

        // starting offset and length
        int begin = offsets[beginIndex];
        int len;
        if (endIndex == offsets.length) {
            len = path.length - begin;
        } else {
            len = offsets[endIndex] - begin - 1;
        }

        // construct result
        byte[] result = new byte[len];
        System.arraycopy(path, begin, result, 0, len);
        return new UnixPath(getFileSystem(), result);
    }


    public boolean isAbsolute() {
        return (path[0] == '/');
    }

    // Resolve child against given base
    private static byte[] resolve(byte[] base, byte[] child) {
        if (child[0] == '/')
            return child;
        byte[] result;
        if (base.length == 1 && base[0] == '/') {
            result = new byte[child.length + 1];
            result[0] = '/';
            System.arraycopy(child, 0, result, 1, child.length);
        } else {
            result = new byte[base.length + 1 + child.length];
            System.arraycopy(base, 0, result, 0, base.length);
            result[base.length] = '/';
            System.arraycopy(child, 0, result,  base.length+1, child.length);
        }
        return result;
    }


    public UnixPath resolve(Path obj) {
        if (obj == null)
            return this;
        byte[] other = checkPath(obj).path;
        if (other[0] == '/')
            return ((UnixPath)obj);
        byte[] result = resolve(path, other);
        return new UnixPath(getFileSystem(), result);
    }


    public UnixPath resolve(String other) {
        return resolve(new UnixPath(getFileSystem(), other));
    }

    UnixPath resolve(byte[] other) {
        return resolve(new UnixPath(getFileSystem(), other));
    }


    public UnixPath relativize(Path obj) {
        UnixPath other = checkPath(obj);
        if (other.equals(this))
            return null;

        // can only relativize paths of the same type
        if (this.isAbsolute() != other.isAbsolute())
            throw new IllegalArgumentException("'other' is different type of Path");

        int bn = this.getNameCount();
        int cn = other.getNameCount();

        // skip matching names
        int n = (bn > cn) ? cn : bn;
        int i = 0;
        while (i < n) {
            if (!this.getName(i).equals(other.getName(i)))
                break;
            i++;
        }

        int dotdots = bn - i;
        if (i < cn) {
            // remaining name components in other
            UnixPath remainder = other.subpath(i, cn);
            if (dotdots == 0)
                return remainder;

            // result is a  "../" for each remaining name in base
            // followed by the remaining names in other
            byte[] result = new byte[dotdots*3 + remainder.path.length];
            int pos = 0;
            while (dotdots > 0) {
                result[pos++] = (byte)'.';
                result[pos++] = (byte)'.';
                result[pos++] = (byte)'/';
                dotdots--;
            }
            System.arraycopy(remainder.path, 0, result, pos, remainder.path.length);
            return new UnixPath(getFileSystem(), result);
        } else {
            // no remaining names in other so result is simply a sequence of ".."
            byte[] result = new byte[dotdots*3 - 1];
            int pos = 0;
            while (dotdots > 0) {
                result[pos++] = (byte)'.';
                result[pos++] = (byte)'.';
                // no tailing slash at the end
                if (dotdots > 1)
                    result[pos++] = (byte)'/';
                dotdots--;
            }
            return new UnixPath(getFileSystem(), result);
        }
    }


    public Path normalize() {
        final int count = getNameCount();
        if (count == 0)
            return this;

        boolean[] ignore = new boolean[count];      // true => ignore name
        int[] size = new int[count];                // length of name
        int remaining = count;                      // number of names remaining
        boolean hasDotDot = false;                  // has at least one ..
        boolean isAbsolute = path[0] == '/';

        // first pass:
        //   1. compute length of names
        //   2. mark all occurences of "." to ignore
        //   3. and look for any occurences of ".."
        for (int i=0; i<count; i++) {
            int begin = offsets[i];
            int len;
            if (i == (offsets.length-1)) {
                len = path.length - begin;
            } else {
                len = offsets[i+1] - begin - 1;
            }
            size[i] = len;

            if (path[begin] == '.') {
                if (len == 1) {
                    ignore[i] = true;  // ignore  "."
                    remaining--;
                }
                else {
                    if (path[begin+1] == '.')   // ".." found
                        hasDotDot = true;
                }
            }
        }

        // multiple passes to eliminate all occurences of name/..
        if (hasDotDot) {
            int prevRemaining;
            do {
                prevRemaining = remaining;
                int prevName = -1;
                for (int i=0; i<count; i++) {
                    if (ignore[i])
                        continue;

                    // not a ".."
                    if (size[i] != 2) {
                        prevName = i;
                        continue;
                    }

                    int begin = offsets[i];
                    if (path[begin] != '.' || path[begin+1] != '.') {
                        prevName = i;
                        continue;
                    }

                    // ".." found
                    if (prevName >= 0) {
                        // name/<ignored>/.. found so mark name and ".." to be
                        // ignored
                        ignore[prevName] = true;
                        ignore[i] = true;
                        remaining = remaining - 2;
                        prevName = -1;
                    } else {
                        // Case: /<ignored>/.. so mark ".." as ignored
                        if (isAbsolute) {
                            boolean hasPrevious = false;
                            for (int j=0; j<i; j++) {
                                if (!ignore[j]) {
                                    hasPrevious = true;
                                    break;
                                }
                            }
                            if (!hasPrevious) {
                                // all proceeding names are ignored
                                ignore[i] = true;
                                remaining--;
                            }
                        }
                    }
                }
            } while (prevRemaining > remaining);
        }

        // no redundant names
        if (remaining == count)
            return this;

        // corner case - all names removed
        if (remaining == 0) {
            return isAbsolute ? getFileSystem().rootDirectory() : null;
        }

        // compute length of result
        int len = remaining - 1;
        if (isAbsolute)
            len++;

        for (int i=0; i<count; i++) {
            if (!ignore[i])
                len += size[i];
        }
        byte[] result = new byte[len];

        // copy names into result
        int pos = 0;
        if (isAbsolute)
            result[pos++] = '/';
        for (int i=0; i<count; i++) {
            if (!ignore[i]) {
                System.arraycopy(path, offsets[i], result, pos, size[i]);
                pos += size[i];
                if (--remaining > 0) {
                    result[pos++] = '/';
                }
            }
        }
        return new UnixPath(getFileSystem(), result);
    }


    public boolean startsWith(Path other) {
        UnixPath that = checkPath(other);

        // other path is longer
        if (that.path.length > path.length) {
            return false;
        }

        int thisOffsetCount = getNameCount();
        int thatOffsetCount = that.getNameCount();

        // given path has more elements that this path
        if (thatOffsetCount > thisOffsetCount) {
            return false;
        }

        // same number of elements so must be exact match
        if ((thatOffsetCount == thisOffsetCount) &&
            (path.length != that.path.length)) {
            return false;
        }

        // check offsets of elements match
        for (int i=0; i<thatOffsetCount; i++) {
            Integer o1 = offsets[i];
            Integer o2 = that.offsets[i];
            if (!o1.equals(o2))
                return false;
        }

        // offsets match so need to compare bytes
        int i=0;
        while (i < that.path.length) {
            if (this.path[i] != that.path[i])
                return false;
            i++;
        }

        // final check that match is on name boundary
        if (i < path.length && this.path[i] != '/') {
            return false;
        }

        return true;
    }


    public boolean endsWith(Path other) {
        UnixPath that = checkPath(other);

        // other path is longer
        if (that.path.length > path.length) {
            return false;
        }

        int thisOffsetCount = getNameCount();
        int thatOffsetCount = that.getNameCount();

        // given path has more elements that this path
        if (thatOffsetCount > thisOffsetCount) {
            return false;
        }

        // same number of elements so must be same length
        if (thatOffsetCount == thisOffsetCount) {
            if (path.length != that.path.length)
                return false;
            if (thisOffsetCount == 0)
                return true;
        }

        // this path has more elements so given path must be relative
        if (thatOffsetCount < thisOffsetCount && that.isAbsolute()) {
            return false;
        }

        // compare bytes
        int thisPos = offsets[thisOffsetCount - thatOffsetCount];
        int thisRemaining = path.length - thisPos;
        if (thisRemaining != that.path.length)
            return false;
        int thatPos = that.offsets[0];
        while (thatPos < that.path.length) {
            if (this.path[thisPos++] != that.path[thatPos++])
                return false;
        }

        return true;
    }


    public int compareTo(Path other) {
        int len1 = path.length;
        int len2 = ((UnixPath) other).path.length;

        int n = Math.min(len1, len2);
        byte v1[] = path;
        byte v2[] = ((UnixPath) other).path;

        int k = 0;
        while (k < n) {
            int c1 = v1[k] & 0xff;
            int c2 = v2[k] & 0xff;
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }


    public boolean equals(Object ob) {
        if ((ob != null) && (ob instanceof UnixPath)) {
            return compareTo((Path)ob) == 0;
        }
        return false;
    }


    public int hashCode() {
        // OK if two or more threads compute hash
        int h = hash;
        if (h == 0) {
            for (int i = 0; i< path.length; i++) {
                h = 31*h + (path[i] & 0xff);
            }
            hash = h;
        }
        return h;
    }


    public String toString() {
        // OK if two or more threads create a String
        if (stringValue == null)
            stringValue = new String(path);     // platform encoding
        return stringValue;
    }


    public Iterator<Path> iterator() {
        initOffsets();
        return new Iterator<Path>() {
            int i = 0;

            public boolean hasNext() {
                return (i < offsets.length);
            }

            public Path next() {
                if (i < offsets.length) {
                    Path result = getName(i);
                    i++;
                    return result;
                } else {
                    throw new NoSuchElementException();
                }
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }

    // -- file operations --

    // package-private
    int openForAttributeAccess(boolean followLinks) throws IOException {
        int flags = O_RDONLY;
        if (!followLinks)
            flags |= O_NOFOLLOW;
        try {
            return open(this, flags, 0);
        } catch (UnixException x) {
            // HACK: EINVAL instead of ELOOP on Solaris 10 prior to u4 (see 6460380)
            if (getFileSystem().isSolaris() && x.errno() == EINVAL)
                x.setError(ELOOP);

            if (x.errno() == ELOOP)
                throw new FileSystemException(getPathForExecptionMessage(), null,
                    x.getMessage() + " or unable to access attributes of symbolic link");

            x.rethrowAsIOException(this);
            return -1; // keep compile happy
        }
    }

    // create file permissions used for read and write checks
    private void checkReadOrWrite(boolean checkRead) {
        SecurityManager sm = System.getSecurityManager();
        if (sm == null)
            return;
        if (perms == null) {
            synchronized (this) {
                if (perms == null) {
                    FilePermission[] p = new FilePermission[2];
                    String pathForPermCheck = getPathForPermissionCheck();
                    p[0] = new FilePermission(pathForPermCheck,
                        SecurityConstants.FILE_READ_ACTION);
                    p[1] = new FilePermission(pathForPermCheck,
                        SecurityConstants.FILE_WRITE_ACTION);
                    perms = p;
                }
            }
        }
        if (checkRead) {
            sm.checkPermission(perms[0]);
        } else {
            sm.checkPermission(perms[1]);
        }
    }

    void checkRead() {
        checkReadOrWrite(true);
    }

    void checkWrite() {
        checkReadOrWrite(false);
    }

    void checkDelete() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // permission not cached
            sm.checkDelete(getPathForPermissionCheck());
        }
    }


    public FileStore getFileStore()
        throws IOException
    {
        checkRead();
        return getFileSystem().getFileStore(this);
    }


    public void checkAccess(AccessMode... modes) throws IOException {
        boolean e = false;
        boolean r = false;
        boolean w = false;
        boolean x = false;

        if (modes.length == 0) {
            e = true;
        } else {
            for (AccessMode mode: modes) {
                switch (mode) {
                    case READ : r = true; break;
                    case WRITE : w = true; break;
                    case EXECUTE : x = true; break;
                    default: throw new AssertionError("Should not get here");
                }
            }
        }

        int mode = 0;
        if (e || r) {
            checkRead();
            mode |= (r) ? R_OK : F_OK;
        }
        if (w) {
            checkWrite();
            mode |= W_OK;
        }
        if (x) {
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                // not cached
                sm.checkExec(getPathForPermissionCheck());
            }
            mode |= X_OK;
        }
        try {
            access(this, mode);
        } catch (UnixException exc) {
            exc.rethrowAsIOException(this);
        }
    }


    public void delete(boolean failIfNotExists) throws IOException {
        checkDelete();

        // need file attributes to know if file is directory
        UnixFileAttributes attrs = null;
        try {
            attrs = UnixFileAttributes.get(this, false);
            if (attrs.isDirectory()) {
                rmdir(this);
            } else {
                unlink(this);
            }
        } catch (UnixException x) {
            // no-op if file does not exist
            if (!failIfNotExists && x.errno() == ENOENT)
                return;

            // DirectoryNotEmptyException if not empty
            if (attrs != null && attrs.isDirectory() &&
                (x.errno() == EEXIST || x.errno() == ENOTEMPTY))
                throw new DirectoryNotEmptyException(getPathForExecptionMessage());

            x.rethrowAsIOException(this);
        }
    }


    public DirectoryStream<Path> newDirectoryStream(DirectoryStream.Filter<? super Path> filter)
        throws IOException
    {
        if (filter == null)
            throw new NullPointerException();
        checkRead();

        // can't return SecureDirectoryStream on older kernels.
        if (!getFileSystem().supportsSecureDirectoryStreams()) {
            try {
                long ptr = opendir(this);
                return new UnixDirectoryStream(this, ptr, filter);
            } catch (UnixException x) {
                if (x.errno() == UnixConstants.ENOTDIR)
                    throw new NotDirectoryException(getPathForExecptionMessage());
                x.rethrowAsIOException(this);
            }
        }

        // open directory and dup file descriptor for use by
        // opendir/readdir/closedir
        int dfd1 = -1;
        int dfd2 = -1;
        long dp = 0L;
        try {
            dfd1 = open(this, O_RDONLY, 0);
            dfd2 = dup(dfd1);
            dp = fdopendir(dfd1);
        } catch (UnixException x) {
            if (dfd1 != -1)
                close(dfd1);
            if (dfd2 != -1)
                close(dfd2);
            if (x.errno() == UnixConstants.ENOTDIR)
                throw new NotDirectoryException(getPathForExecptionMessage());
            x.rethrowAsIOException(this);
        }
        return new UnixSecureDirectoryStream(this, dp, dfd2, filter);
    }

    // invoked by AbstractPath#copyTo

    public void implCopyTo(Path obj, CopyOption... options)
        throws IOException
    {
        UnixPath target = (UnixPath)obj;
        UnixCopyFile.copy(this, target, options);
    }


    public void implMoveTo(Path obj, CopyOption... options)
        throws IOException
    {
        UnixPath target = (UnixPath)obj;
        UnixCopyFile.move(this, target, options);
    }


    @SuppressWarnings("unchecked")
    public <V extends FileAttributeView> V
        getFileAttributeView(Class<V> type, LinkOption... options)
    {
        FileAttributeView view = getFileSystem()
            .newFileAttributeView(type, this, options);
        if (view == null)
            return null;
        return (V) view;
    }


    public FileAttributeView getFileAttributeView(String name, LinkOption... options) {
        return getFileSystem().newFileAttributeView(name, this, options);
    }


    public Path createDirectory(FileAttribute<?>... attrs)
        throws IOException
    {
        checkWrite();

        int mode = UnixFileModeAttribute
            .toUnixMode(UnixFileModeAttribute.ALL_PERMISSIONS, attrs);
        try {
            mkdir(this, mode);
        } catch (UnixException x) {
            x.rethrowAsIOException(this);
        }
        return this;
    }


    public InputStream newInputStream()throws IOException {
        try {
            Set<OpenOption> options = Collections.emptySet();
            FileChannel fc = UnixChannelFactory.newFileChannel(this, options, 0);
            return Channels.newInputStream(fc);
        } catch (UnixException x) {
            x.rethrowAsIOException(this);
            return null;  // keep compiler happy
        }
    }


    public SeekableByteChannel newByteChannel(Set<? extends OpenOption> options,
                                                      FileAttribute<?>... attrs)
         throws IOException
    {
        int mode = UnixFileModeAttribute
            .toUnixMode(UnixFileModeAttribute.ALL_READWRITE, attrs);
        try {
            return UnixChannelFactory.newFileChannel(this, options, mode);
        } catch (UnixException x) {
            x.rethrowAsIOException(this);
            return null;  // keep compiler happy
        }
    }


    public OutputStream newOutputStream(Set<? extends OpenOption> options,
                                        FileAttribute<?>... attrs)
        throws IOException
    {
        // need to copy options to add WRITE
        Set<OpenOption> opts = new HashSet<OpenOption>(options);
        if (opts.contains(StandardOpenOption.READ))
            throw new IllegalArgumentException("READ not allowed");
        opts.add(StandardOpenOption.WRITE);

        int mode = UnixFileModeAttribute
            .toUnixMode(UnixFileModeAttribute.ALL_READWRITE, attrs);
        try {
            FileChannel fc = UnixChannelFactory.newFileChannel(this, opts, mode);
            return Channels.newOutputStream(fc);
        } catch (UnixException x) {
            x.rethrowAsIOException(this);
            return null;  // keep compiler happy
        }
    }


    public boolean isSameFile(FileRef obj) throws IOException {
        if (this.equals(obj))
            return true;
        if (!(obj instanceof UnixPath))  // includes null check
            return false;
        UnixPath other = (UnixPath)obj;

        // check security manager access to both files
        this.checkRead();
        other.checkRead();

        UnixFileAttributes thisAttrs;
        UnixFileAttributes otherAttrs;
        try {
             thisAttrs = UnixFileAttributes.get(this, true);
        } catch (UnixException x) {
            x.rethrowAsIOException(this);
            return false;    // keep compiler happy
        }
        try {
            otherAttrs = UnixFileAttributes.get(other, true);
        } catch (UnixException x) {
            x.rethrowAsIOException(other);
            return false;    // keep compiler happy
        }
        return thisAttrs.isSameFile(otherAttrs);
    }


    public Path createSymbolicLink(Path obj, FileAttribute<?>... attrs)
        throws IOException
    {
        UnixPath target = checkPath(obj);

        // no attributes supported when creating links
        for (FileAttribute<?> attr: attrs) {
            if (attr == null)
                throw new NullPointerException();
            throw new ProviderMismatchException();
        }

        // permission check
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new LinkPermission("symbolic"));
            checkWrite();
        }

        // create link
        try {
            symlink(target.asByteArray(), this);
        } catch (UnixException x) {
            x.rethrowAsIOException(this);
        }

        return this;
    }


    public Path createLink(Path obj) throws IOException {
        UnixPath existing = checkPath(obj);

        // permission check
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new LinkPermission("hard"));
            this.checkWrite();
            existing.checkWrite();
        }
        try {
            link(existing, this);
        } catch (UnixException x) {
            x.rethrowAsIOException(this, existing);
        }
        return this;
    }


    public Path readSymbolicLink() throws IOException {
        // permission check
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            FilePermission perm = new FilePermission(getPathForPermissionCheck(),
                SecurityConstants.FILE_READLINK_ACTION);
            AccessController.checkPermission(perm);
        }
        try {
            byte[] target = readlink(this);
            return new UnixPath(getFileSystem(), target);
        } catch (UnixException x) {
           if (x.errno() == UnixConstants.EINVAL)
                throw new NotLinkException(getPathForExecptionMessage());
            x.rethrowAsIOException(this);
            return null;    // keep compiler happy
        }
    }


    public UnixPath toAbsolutePath() {
        if (isAbsolute()) {
            return this;
        }
        // The path is relative so need to resolve against default directory,
        // taking care not to reveal the user.dir
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPropertyAccess("user.dir");
        }
        return new UnixPath(getFileSystem(),
            resolve(getFileSystem().defaultDirectory(), path));
    }


    public UnixPath toRealPath(boolean resolveLinks) throws IOException {
        checkRead();

        UnixPath absolute = toAbsolutePath();

        // if resolveLinks is true then use realpath
        if (resolveLinks) {
            try {
                byte[] rp = realpath(absolute);
                return new UnixPath(getFileSystem(), rp);
            } catch (UnixException x) {
                x.rethrowAsIOException(this);
            }
        }

        // if resolveLinks is false then eliminate "." and also ".."
        // where the previous element is not a link.
        UnixPath root = getFileSystem().rootDirectory();
        UnixPath result = root;
        for (int i=0; i<absolute.getNameCount(); i++) {
            UnixPath element = absolute.getName(i);

            // eliminate "."
            if ((element.asByteArray().length == 1) && (element.asByteArray()[0] == '.'))
                continue;

            // cannot eliminate ".." if previous element is a link
            if ((element.asByteArray().length == 2) && (element.asByteArray()[0] == '.') &&
                (element.asByteArray()[1] == '.'))
            {
                UnixFileAttributes attrs = null;
                try {
                    attrs = UnixFileAttributes.get(result, false);
                } catch (UnixException x) {
                    x.rethrowAsIOException(result);
                }
                if (!attrs.isSymbolicLink()) {
                    result = result.getParent();
                    if (result == null) {
                        result = root;
                    }
                    continue;
                }
            }
            result = result.resolve(element);
        }

        // finally check that file exists
        try {
            UnixFileAttributes.get(result, true);
        } catch (UnixException x) {
            x.rethrowAsIOException(result);
        }
        return result;
    }


    public boolean isHidden() {
        checkRead();
        UnixPath name = getName();
        if (name == null)
            return false;
        return (name.asByteArray()[0] == '.');
    }


    public URI toUri() {
        return UnixUriUtils.toUri(this);
    }


    public WatchKey register(WatchService watcher,
                             WatchEvent.Kind<?>[] events,
                             WatchEvent.Modifier... modifiers)
        throws IOException
    {
        if (watcher == null)
            throw new NullPointerException();
        if (!(watcher instanceof AbstractWatchService))
            throw new ProviderMismatchException();
        checkRead();
        return ((AbstractWatchService)watcher).register(this, events, modifiers);
    }
}
