1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18package java.io;
19
20import java.security.AccessController;
21import java.security.Permission;
22import java.security.PermissionCollection;
23import java.security.PrivilegedAction;
24
25import org.apache.harmony.luni.util.Msg;
26
27/**
28 * A permission for accessing a file or directory. The FilePermission is made up
29 * of a pathname and a set of actions which are valid for the pathname.
30 * <p>
31 * The {@code File.separatorChar} must be used in all pathnames when
32 * constructing a FilePermission. The following descriptions will assume the
33 * char is {@code /}. A pathname that ends in {@code /*} includes all the files
34 * and directories contained in that directory. If the pathname
35 * ends in {@code /-}, it includes all the files and directories in that
36 * directory <i>recursively</i>. The following pathnames have a special meaning:
37 * <ul>
38 *   <li>
39 *     "*": all files in the current directory;
40 *   </li>
41 *   <li>
42 *     "-": recursively all files and directories in the current directory;
43 *   </li>
44 *   <li>
45 *     "&lt;&lt;ALL FILES&gt;&gt;": any file and directory in the file system.
46 *   </li>
47 * </ul>
48 */
49public final class FilePermission extends Permission implements Serializable {
50
51    private static final long serialVersionUID = 7930732926638008763L;
52
53    // canonical path of this permission
54    private transient String canonPath;
55
56    // list of actions permitted for socket permission in order
57    private static final String[] actionList = { "read", "write", "execute", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
58            "delete" }; //$NON-NLS-1$
59
60    // "canonicalized" action list
61    private String actions;
62
63    // the numeric representation of this action list
64    // for implies() to check if one action list is the subset of another.
65    transient int mask = -1;
66
67    // global include all permission?
68    private transient boolean includeAll = false;
69
70    private transient boolean allDir = false;
71
72    private transient boolean allSubdir = false;
73
74    /**
75     * Constructs a new FilePermission with the path and actions specified.
76     *
77     * @param path
78     *            the pathname of the file or directory to apply the actions to.
79     * @param actions
80     *            the actions for the {@code path}. May be any combination of
81     *            "read", "write", "execute" and "delete".
82     * @throws IllegalArgumentException
83     *             if {@code actions} is {@code null} or an empty string, or if
84     *             it contains a string other than "read", "write", "execute"
85     *             and "delete".
86     * @throws NullPointerException
87     *             if {@code path} is {@code null}.
88     */
89    public FilePermission(String path, String actions) {
90        super(path);
91        init(path, actions);
92    }
93
94    private void init(final String path, String pathActions) {
95        if (pathActions == null || pathActions.equals("")) { //$NON-NLS-1$
96            throw new IllegalArgumentException(Msg.getString("K006d")); //$NON-NLS-1$
97        }
98        this.actions = toCanonicalActionString(pathActions);
99
100        if (path == null) {
101            throw new NullPointerException(Msg.getString("K006e")); //$NON-NLS-1$
102        }
103        if (path.equals("<<ALL FILES>>")) { //$NON-NLS-1$
104            includeAll = true;
105        } else {
106            canonPath = AccessController
107                    .doPrivileged(new PrivilegedAction<String>() {
108                        public String run() {
109                            try {
110                                return new File(path).getCanonicalPath();
111                            } catch (IOException e) {
112                                return path;
113                            }
114                        }
115                    });
116            if (path.equals("*") || path.endsWith(File.separator + "*")) { //$NON-NLS-1$ //$NON-NLS-2$
117                allDir = true;
118            }
119            if (path.equals("-") || path.endsWith(File.separator + "-")) { //$NON-NLS-1$ //$NON-NLS-2$
120                allSubdir = true;
121            }
122        }
123    }
124
125    /**
126     * Returns the string representing this permission's actions. It must be of
127     * the form "read,write,execute,delete", all lower case and in the correct
128     * order if there is more than one action.
129     *
130     * @param action
131     *            the action name
132     * @return the string representing this permission's actions
133     */
134    private String toCanonicalActionString(String action) {
135        actions = action.trim().toLowerCase();
136
137        // get the numerical representation of the action list
138        mask = getMask(actions);
139
140        // convert the mask to a canonical action list.
141        int len = actionList.length;
142        // the test mask - shift the 1 to the leftmost position of the
143        // actionList
144        int highestBitMask = 1 << (len - 1);
145
146        // if a bit of mask is set, append the corresponding action to result
147        StringBuilder result = new StringBuilder();
148        boolean addedItem = false;
149        for (int i = 0; i < len; i++) {
150            if ((highestBitMask & mask) != 0) {
151                if (addedItem) {
152                    result.append(","); //$NON-NLS-1$
153                }
154                result.append(actionList[i]);
155                addedItem = true;
156            }
157            highestBitMask = highestBitMask >> 1;
158        }
159        return result.toString();
160    }
161
162    /**
163     * Returns the numerical representation of the argument.
164     *
165     * @param actionNames
166     *            the action names
167     * @return the action mask
168     */
169    private int getMask(String actionNames) {
170        int actionInt = 0, head = 0, tail = 0;
171        do {
172            tail = actionNames.indexOf(",", head); //$NON-NLS-1$
173            String action = tail > 0 ? actionNames.substring(head, tail).trim()
174                    : actionNames.substring(head).trim();
175            if (action.equals("read")) { //$NON-NLS-1$
176                actionInt |= 8;
177            } else if (action.equals("write")) { //$NON-NLS-1$
178                actionInt |= 4;
179            } else if (action.equals("execute")) { //$NON-NLS-1$
180                actionInt |= 2;
181            } else if (action.equals("delete")) { //$NON-NLS-1$
182                actionInt |= 1;
183            } else {
184                throw new IllegalArgumentException(Msg.getString(
185                        "K006f", action)); //$NON-NLS-1$
186            }
187            head = tail + 1;
188        } while (tail > 0);
189        return actionInt;
190    }
191
192    /**
193     * Returns the actions associated with this file permission.
194     *
195     * @return the actions associated with this file permission.
196     */
197    @Override
198    public String getActions() {
199        return actions;
200    }
201
202    /**
203     * Indicates if this file permission is equal to another. The two are equal
204     * if {@code obj} is a FilePermission, they have the same path, and they
205     * have the same actions.
206     *
207     * @param obj
208     *            the object to check equality with.
209     * @return {@code true} if this file permission is equal to {@code obj},
210     *         {@code false} otherwise.
211     */
212    @Override
213    public boolean equals(Object obj) {
214        if (obj instanceof FilePermission) {
215            FilePermission fp = (FilePermission) obj;
216            if (fp.actions != actions) {
217                if (fp.actions == null || !fp.actions.equals(actions)) {
218                    return false;
219                }
220            }
221
222            /* Matching actions and both are <<ALL FILES>> ? */
223            if (fp.includeAll || includeAll) {
224                return fp.includeAll == includeAll;
225            }
226            return fp.canonPath.equals(canonPath);
227        }
228        return false;
229    }
230
231    /**
232     * Indicates whether the permission {@code p} is implied by this file
233     * permission. This is the case if {@code p} is an instance of
234     * {@code FilePermission}, if {@code p}'s actions are a subset of this
235     * file permission's actions and if {@code p}'s path is implied by this
236     * file permission's path.
237     *
238     * @param p
239     *            the permission to check.
240     * @return {@code true} if the argument permission is implied by the
241     *         receiver, and {@code false} if it is not.
242     */
243    @Override
244    public boolean implies(Permission p) {
245        int match = impliesMask(p);
246        return match != 0 && match == ((FilePermission) p).mask;
247    }
248
249    /**
250     * Returns an int describing what masks are implied by a specific
251     * permission.
252     *
253     * @param p
254     *            the permission
255     * @return the mask applied to the given permission
256     */
257    int impliesMask(Permission p) {
258        if (!(p instanceof FilePermission)) {
259            return 0;
260        }
261        FilePermission fp = (FilePermission) p;
262        int matchedMask = mask & fp.mask;
263        // Can't match any bits?
264        if (matchedMask == 0) {
265            return 0;
266        }
267
268        // Is this permission <<ALL FILES>>
269        if (includeAll) {
270            return matchedMask;
271        }
272
273        // We can't imply all files
274        if (fp.includeAll) {
275            return 0;
276        }
277
278        // Scan the length of p checking all match possibilities
279        // \- implies everything except \
280        int thisLength = canonPath.length();
281        if (allSubdir && thisLength == 2
282                && !fp.canonPath.equals(File.separator)) {
283            return matchedMask;
284        }
285        // need /- to imply /-
286        if (fp.allSubdir && !allSubdir) {
287            return 0;
288        }
289        // need /- or /* to imply /*
290        if (fp.allDir && !allSubdir && !allDir) {
291            return 0;
292        }
293
294        boolean includeDir = false;
295        int pLength = fp.canonPath.length();
296        // do not compare the * or -
297        if (allDir || allSubdir) {
298            thisLength--;
299        }
300        if (fp.allDir || fp.allSubdir) {
301            pLength--;
302        }
303        for (int i = 0; i < pLength; i++) {
304            char pChar = fp.canonPath.charAt(i);
305            // Is p longer than this permissions canonLength?
306            if (i >= thisLength) {
307                if (i == thisLength) {
308                    // Is this permission include all? (must have matched up
309                    // until this point).
310                    if (allSubdir) {
311                        return matchedMask;
312                    }
313                    // Is this permission include a dir? Continue the check
314                    // afterwards.
315                    if (allDir) {
316                        includeDir = true;
317                    }
318                }
319                // If not includeDir then is has to be a mismatch.
320                if (!includeDir) {
321                    return 0;
322                }
323                /**
324                 * If we have * for this and find a separator it is invalid. IE:
325                 * this is '/a/*' and p is '/a/b/c' we should fail on the
326                 * separator after the b. Except for root, canonical paths do
327                 * not end in a separator.
328                 */
329                if (pChar == File.separatorChar) {
330                    return 0;
331                }
332            } else {
333                // Are the characters matched?
334                if (canonPath.charAt(i) != pChar) {
335                    return 0;
336                }
337            }
338        }
339        // Must have matched up to this point or it's a valid file in an include
340        // all directory
341        if (pLength == thisLength) {
342            if (allSubdir) {
343                // /- implies /- or /*
344                return fp.allSubdir || fp.allDir ? matchedMask : 0;
345            }
346            return allDir == fp.allDir ? matchedMask : 0;
347        }
348        return includeDir ? matchedMask : 0;
349    }
350
351    /**
352     * Returns a new PermissionCollection in which to place FilePermission
353     * objects.
354     *
355     * @return A new PermissionCollection object suitable for storing
356     *         FilePermission objects.
357     */
358    @Override
359    public PermissionCollection newPermissionCollection() {
360        return new FilePermissionCollection();
361    }
362
363    /**
364     * Calculates the hash code value for this file permission.
365     *
366     * @return the hash code value for this file permission.
367     */
368    @Override
369    public int hashCode() {
370        return (canonPath == null ? getName().hashCode() : canonPath.hashCode())
371                + mask;
372    }
373
374    private void writeObject(ObjectOutputStream stream) throws IOException {
375        stream.defaultWriteObject();
376    }
377
378    private void readObject(ObjectInputStream stream) throws IOException,
379            ClassNotFoundException {
380        stream.defaultReadObject();
381        init(getName(), actions);
382    }
383}
384