/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package java.io;

import java.security.AccessController;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.PrivilegedAction;

import org.apache.harmony.luni.util.Msg;

/**
 * A permission for accessing a file or directory. The FilePermission is made up
 * of a pathname and a set of actions which are valid for the pathname.
 * <p>
 * The {@code File.separatorChar} must be used in all pathnames when
 * constructing a FilePermission. The following descriptions will assume the
 * char is {@code /}. A pathname that ends in {@code /*} includes all the files
 * and directories contained in that directory. If the pathname
 * ends in {@code /-}, it includes all the files and directories in that
 * directory <i>recursively</i>. The following pathnames have a special meaning:
 * </p>
 * <ul>
 *   <li>
 *     "*": all files in the current directory;
 *   </li>
 *   <li>
 *     "-": recursively all files and directories in the current directory;
 *   </li>
 *   <li>
 *     "&lt;&lt;ALL FILES&gt;&gt;": any file and directory in the file system.
 *   </li>
 * </ul>
 * 
 * @since Android 1.0
 */
public final class FilePermission extends Permission implements Serializable {
    
    private static final long serialVersionUID = 7930732926638008763L;

    // canonical path of this permission
    private transient String canonPath;

    // list of actions permitted for socket permission in order
    private static final String[] actionList = { "read", "write", "execute", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            "delete" }; //$NON-NLS-1$

    // "canonicalized" action list
    private String actions;

    // the numeric representation of this action list
    // for implies() to check if one action list is the subset of another.
    transient int mask = -1;

    // global include all permission?
    private transient boolean includeAll = false;

    private transient boolean allDir = false;

    private transient boolean allSubdir = false;

    /**
     * Constructs a new FilePermission with the path and actions specified.
     * 
     * @param path
     *            the pathname of the file or directory to apply the actions to.
     * @param actions
     *            the actions for the {@code path}. May be any combination of
     *            "read", "write", "execute" and "delete".
     * @throws IllegalArgumentException
     *             if {@code actions} is {@code null} or an empty string, or if
     *             it contains a string other than "read", "write", "execute"
     *             and "delete".
     * @throws NullPointerException
     *             if {@code path} is null.
     * @since Android 1.0
     */
    public FilePermission(String path, String actions) {
        super(path);
        init(path, actions);
    }

    private void init(final String path, String pathActions) {
        if (pathActions == null || pathActions.equals("")) { //$NON-NLS-1$
            throw new IllegalArgumentException(Msg.getString("K006d")); //$NON-NLS-1$
        }
        this.actions = toCanonicalActionString(pathActions);

        if (path == null) {
            throw new NullPointerException(Msg.getString("K006e")); //$NON-NLS-1$
        }
        if (path.equals("<<ALL FILES>>")) { //$NON-NLS-1$
            includeAll = true;
        } else {
            canonPath = AccessController
                    .doPrivileged(new PrivilegedAction<String>() {
                        public String run() {
                            try {
                                return new File(path).getCanonicalPath();
                            } catch (IOException e) {
                                return path;
                            }
                        }
                    });
            if (path.equals("*") || path.endsWith(File.separator + "*")) { //$NON-NLS-1$ //$NON-NLS-2$
                allDir = true;
            }
            if (path.equals("-") || path.endsWith(File.separator + "-")) { //$NON-NLS-1$ //$NON-NLS-2$
                allSubdir = true;
            }
        }
    }

    /**
     * Returns the string representing this permission's actions. It must be of
     * the form "read,write,execute,delete", all lower case and in the correct
     * order if there is more than one action.
     * 
     * @param action
     *            the action name
     * @return the string representing this permission's actions
     */
    private String toCanonicalActionString(String action) {
        actions = action.trim().toLowerCase();

        // get the numerical representation of the action list
        mask = getMask(actions);

        // convert the mask to a canonical action list.
        int len = actionList.length;
        // the test mask - shift the 1 to the leftmost position of the
        // actionList
        int highestBitMask = 1 << (len - 1);

        // if a bit of mask is set, append the corresponding action to result
        StringBuilder result = new StringBuilder();
        boolean addedItem = false;
        for (int i = 0; i < len; i++) {
            if ((highestBitMask & mask) != 0) {
                if (addedItem) {
                    result.append(","); //$NON-NLS-1$
                }
                result.append(actionList[i]);
                addedItem = true;
            }
            highestBitMask = highestBitMask >> 1;
        }
        return result.toString();
    }

    /**
     * Returns the numerical representation of the argument.
     * 
     * @param actionNames
     *            the action names
     * @return the action mask
     */
    private int getMask(String actionNames) {
        int actionInt = 0, head = 0, tail = 0;
        do {
            tail = actionNames.indexOf(",", head); //$NON-NLS-1$
            String action = tail > 0 ? actionNames.substring(head, tail).trim()
                    : actionNames.substring(head).trim();
            if (action.equals("read")) { //$NON-NLS-1$
                actionInt |= 8;
            } else if (action.equals("write")) { //$NON-NLS-1$
                actionInt |= 4;
            } else if (action.equals("execute")) { //$NON-NLS-1$
                actionInt |= 2;
            } else if (action.equals("delete")) { //$NON-NLS-1$
                actionInt |= 1;
            } else {
                throw new IllegalArgumentException(Msg.getString(
                        "K006f", action)); //$NON-NLS-1$
            }
            head = tail + 1;
        } while (tail > 0);
        return actionInt;
    }

    /**
     * Returns the actions associated with this file permission.
     * 
     * @return the actions associated with this file permission.
     * @since Android 1.0
     */
    @Override
    public String getActions() {
        return actions;
    }

    /**
     * Indicates if this file permission is equal to another. The two are equal
     * if {@code obj} is a FilePermission, they have the same path, and they
     * have the same actions.
     * 
     * @param obj
     *            the object to check equality with.
     * @return {@code true} if this file permission is equal to {@code obj},
     *         {@code false} otherwise.
     * @since Android 1.0
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof FilePermission) {
            FilePermission fp = (FilePermission) obj;
            if (fp.actions != actions) {
                if (fp.actions == null || !fp.actions.equals(actions)) {
                    return false;
                }
            }

            /* Matching actions and both are <<ALL FILES>> ? */
            if (fp.includeAll || includeAll) {
                return fp.includeAll == includeAll;
            }
            return fp.canonPath.equals(canonPath);
        }
        return false;
    }

    /**
     * Indicates whether the permission {@code p} is implied by this file
     * permission. This is the case if {@code p} is an instance of
     * {@code FilePermission}, if {@code p}'s actions are a subset of this
     * file permission's actions and if {@code p}'s path is implied by this
     * file permission's path.
     * 
     * @param p
     *            the permission to check.
     * @return {@code true} if the argument permission is implied by the
     *         receiver, and {@code false} if it is not.
     * @since Android 1.0
     */
    @Override
    public boolean implies(Permission p) {
        int match = impliesMask(p);
        return match != 0 && match == ((FilePermission) p).mask;
    }

    /**
     * Returns an int describing what masks are implied by a specific
     * permission.
     * 
     * @param p
     *            the permission
     * @return the mask applied to the given permission
     */
    int impliesMask(Permission p) {
        if (!(p instanceof FilePermission)) {
            return 0;
        }
        FilePermission fp = (FilePermission) p;
        int matchedMask = mask & fp.mask;
        // Can't match any bits?
        if (matchedMask == 0) {
            return 0;
        }

        // Is this permission <<ALL FILES>>
        if (includeAll) {
            return matchedMask;
        }

        // We can't imply all files
        if (fp.includeAll) {
            return 0;
        }

        // Scan the length of p checking all match possibilities
        // \- implies everything except \
        int thisLength = canonPath.length();
        if (allSubdir && thisLength == 2
                && !fp.canonPath.equals(File.separator)) {
            return matchedMask;
        }
        // need /- to imply /-
        if (fp.allSubdir && !allSubdir) {
            return 0;
        }
        // need /- or /* to imply /*
        if (fp.allDir && !allSubdir && !allDir) {
            return 0;
        }

        boolean includeDir = false;
        int pLength = fp.canonPath.length();
        // do not compare the * or -
        if (allDir || allSubdir) {
            thisLength--;
        }
        if (fp.allDir || fp.allSubdir) {
            pLength--;
        }
        for (int i = 0; i < pLength; i++) {
            char pChar = fp.canonPath.charAt(i);
            // Is p longer than this permissions canonLength?
            if (i >= thisLength) {
                if (i == thisLength) {
                    // Is this permission include all? (must have matched up
                    // until this point).
                    if (allSubdir) {
                        return matchedMask;
                    }
                    // Is this permission include a dir? Continue the check
                    // afterwards.
                    if (allDir) {
                        includeDir = true;
                    }
                }
                // If not includeDir then is has to be a mismatch.
                if (!includeDir) {
                    return 0;
                }
                /**
                 * If we have * for this and find a separator it is invalid. IE:
                 * this is '/a/*' and p is '/a/b/c' we should fail on the
                 * separator after the b. Except for root, canonical paths do
                 * not end in a separator.
                 */
                if (pChar == File.separatorChar) {
                    return 0;
                }
            } else {
                // Are the characters matched?
                if (canonPath.charAt(i) != pChar) {
                    return 0;
                }
            }
        }
        // Must have matched up to this point or it's a valid file in an include
        // all directory
        if (pLength == thisLength) {
            if (allSubdir) {
                // /- implies /- or /*
                return fp.allSubdir || fp.allDir ? matchedMask : 0;
            }
            return allDir == fp.allDir ? matchedMask : 0;
        }
        return includeDir ? matchedMask : 0;
    }

    /**
     * Returns a new PermissionCollection in which to place FilePermission
     * objects.
     * 
     * @return A new PermissionCollection object suitable for storing
     *         FilePermission objects.
     * @since Android 1.0
     */
    @Override
    public PermissionCollection newPermissionCollection() {
        return new FilePermissionCollection();
    }

    /**
     * Calculates the hash code value for this file permission.
     * 
     * @return the hash code value for this file permission.
     * @since Android 1.0
     */
    @Override
    public int hashCode() {
        return (canonPath == null ? getName().hashCode() : canonPath.hashCode())
                + mask;
    }

    private void writeObject(ObjectOutputStream stream) throws IOException {
        stream.defaultWriteObject();
    }

    private void readObject(ObjectInputStream stream) throws IOException,
            ClassNotFoundException {
        stream.defaultReadObject();
        init(getName(), actions);
    }
}
