001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.util;
020
021import org.apache.shiro.lang.util.StringUtils;
022
023/**
024 * <p>PathMatcher implementation for Ant-style path patterns.
025 * Examples are provided below.</p>
026 *
027 * <p>Part of this mapping code has been kindly borrowed from
028 * <a href="http://ant.apache.org">Apache Ant</a>.
029 *
030 * <p>The mapping matches URLs using the following rules:<br>
031 * <ul>
032 * <li>? matches one character</li>
033 * <li>* matches zero or more characters</li>
034 * <li>** matches zero or more 'directories' in a path</li>
035 * </ul>
036 *
037 * <p>Some examples:<br>
038 * <ul>
039 * <li><code>com/t?st.jsp</code> - matches <code>com/test.jsp</code> but also
040 * <code>com/tast.jsp</code> or <code>com/txst.jsp</code></li>
041 * <li><code>com/*.jsp</code> - matches all <code>.jsp</code> files in the
042 * <code>com</code> directory</li>
043 * <li><code>com/&#42;&#42;/test.jsp</code> - matches all <code>test.jsp</code>
044 * files underneath the <code>com</code> path</li>
045 * <li><code>org/apache/shiro/&#42;&#42;/*.jsp</code> - matches all <code>.jsp</code>
046 * files underneath the <code>org/apache/shiro</code> path</li>
047 * <li><code>org/&#42;&#42;/servlet/bla.jsp</code> - matches
048 * <code>org/apache/shiro/servlet/bla.jsp</code> but also
049 * <code>org/apache/shiro/testing/servlet/bla.jsp</code> and
050 * <code>org/servlet/bla.jsp</code></li>
051 * </ul>
052 *
053 * <p><b>N.B.</b>: This class was borrowed (with much appreciation) from the
054 * <a href="http://www.springframework.org">Spring Framework</a> with modifications.  We didn't want to reinvent the
055 * wheel of great work they've done, but also didn't want to force every Shiro user to depend on Spring</p>
056 *
057 * <p>As per the Apache 2.0 license, the original copyright notice and all author and copyright information have
058 * remained in tact.</p>
059 *
060 * @since 16.07.2003
061 */
062public class AntPathMatcher implements PatternMatcher {
063
064    //TODO - complete JavaDoc
065
066    /**
067     * Default path separator: "/"
068     */
069    public static final String DEFAULT_PATH_SEPARATOR = "/";
070
071    private String pathSeparator = DEFAULT_PATH_SEPARATOR;
072
073
074    /**
075     * Set the path separator to use for pattern parsing.
076     * Default is "/", as in Ant.
077     */
078    public void setPathSeparator(String pathSeparator) {
079        this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
080    }
081
082    /**
083     * Checks if {@code path} is a pattern (i.e. contains a '*', or '?').
084     * For example the {@code /foo/**} would return {@code true}, while {@code /bar/} would return {@code false}.
085     *
086     * @param path the string to check
087     * @return this method returns {@code true} if {@code path} contains a '*' or '?', otherwise, {@code false}
088     */
089    public boolean isPattern(String path) {
090        if (path == null) {
091            return false;
092        }
093        return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
094    }
095
096    public boolean matches(String pattern, String source) {
097        return match(pattern, source);
098    }
099
100    public boolean match(String pattern, String path) {
101        return doMatch(pattern, path, true);
102    }
103
104    public boolean matchStart(String pattern, String path) {
105        return doMatch(pattern, path, false);
106    }
107
108    /**
109     * Actually match the given <code>path</code> against the given <code>pattern</code>.
110     *
111     * @param pattern   the pattern to match against
112     * @param path      the path String to test
113     * @param fullMatch whether a full pattern match is required
114     *                  (else a pattern match as far as the given base path goes is sufficient)
115     * @return <code>true</code> if the supplied <code>path</code> matched,
116     * <code>false</code> if it didn't
117     */
118    @SuppressWarnings({"checkstyle:ReturnCount", "checkstyle:CyclomaticComplexity",
119            "checkstyle:NPathComplexity", "checkstyle:MethodLength"})
120    protected boolean doMatch(String pattern, String path, boolean fullMatch) {
121        if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
122            return false;
123        }
124
125        String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, false, true);
126        String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator, false, true);
127
128        int pattIdxStart = 0;
129        int pattIdxEnd = pattDirs.length - 1;
130        int pathIdxStart = 0;
131        int pathIdxEnd = pathDirs.length - 1;
132
133        // Match all elements up to the first **
134        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
135            String patDir = pattDirs[pattIdxStart];
136            if ("**".equals(patDir)) {
137                break;
138            }
139            if (!matchStrings(patDir, pathDirs[pathIdxStart])) {
140                return false;
141            }
142            pattIdxStart++;
143            pathIdxStart++;
144        }
145
146        if (pathIdxStart > pathIdxEnd) {
147            // Path is exhausted, only match if rest of pattern is * or **'s
148            if (pattIdxStart > pattIdxEnd) {
149                return (pattern.endsWith(this.pathSeparator)
150                        ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator));
151            }
152            if (!fullMatch) {
153                return true;
154            }
155            if (pattIdxStart == pattIdxEnd && "*".equals(pattDirs[pattIdxStart])
156                    && path.endsWith(this.pathSeparator)) {
157                return true;
158            }
159            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
160                if (!"**".equals(pattDirs[i])) {
161                    return false;
162                }
163            }
164            return true;
165        } else if (pattIdxStart > pattIdxEnd) {
166            // String not exhausted, but pattern is. Failure.
167            return false;
168        } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
169            // Path start definitely matches due to "**" part in pattern.
170            return true;
171        }
172
173        // up to last '**'
174        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
175            String patDir = pattDirs[pattIdxEnd];
176            if (patDir.equals("**")) {
177                break;
178            }
179            if (!matchStrings(patDir, pathDirs[pathIdxEnd])) {
180                return false;
181            }
182            pattIdxEnd--;
183            pathIdxEnd--;
184        }
185        if (pathIdxStart > pathIdxEnd) {
186            // String is exhausted
187            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
188                if (!pattDirs[i].equals("**")) {
189                    return false;
190                }
191            }
192            return true;
193        }
194
195        while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
196            int patIdxTmp = -1;
197            for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
198                if (pattDirs[i].equals("**")) {
199                    patIdxTmp = i;
200                    break;
201                }
202            }
203            if (patIdxTmp == pattIdxStart + 1) {
204                // '**/**' situation, so skip one
205                pattIdxStart++;
206                continue;
207            }
208            // Find the pattern between padIdxStart & padIdxTmp in str between
209            // strIdxStart & strIdxEnd
210            int patLength = (patIdxTmp - pattIdxStart - 1);
211            int strLength = (pathIdxEnd - pathIdxStart + 1);
212            int foundIdx = -1;
213
214            strLoop:
215            for (int i = 0; i <= strLength - patLength; i++) {
216                for (int j = 0; j < patLength; j++) {
217                    String subPat = (String) pattDirs[pattIdxStart + j + 1];
218                    String subStr = (String) pathDirs[pathIdxStart + i + j];
219                    if (!matchStrings(subPat, subStr)) {
220                        continue strLoop;
221                    }
222                }
223                foundIdx = pathIdxStart + i;
224                break;
225            }
226
227            if (foundIdx == -1) {
228                return false;
229            }
230
231            pattIdxStart = patIdxTmp;
232            pathIdxStart = foundIdx + patLength;
233        }
234
235        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
236            if (!pattDirs[i].equals("**")) {
237                return false;
238            }
239        }
240
241        return true;
242    }
243
244    /**
245     * Tests whether or not a string matches against a pattern.
246     * The pattern may contain two special characters:<br>
247     * '*' means zero or more characters<br>
248     * '?' means one and only one character
249     *
250     * @param pattern pattern to match against.
251     *                Must not be <code>null</code>.
252     * @param str     string which must be matched against the pattern.
253     *                Must not be <code>null</code>.
254     * @return <code>true</code> if the string matches against the
255     * pattern, or <code>false</code> otherwise.
256     */
257    @SuppressWarnings({"checkstyle:ReturnCount", "checkstyle:CyclomaticComplexity",
258            "checkstyle:NPathComplexity", "checkstyle:MethodLength"})
259    private boolean matchStrings(String pattern, String str) {
260        char[] patArr = pattern.toCharArray();
261        char[] strArr = str.toCharArray();
262        int patIdxStart = 0;
263        int patIdxEnd = patArr.length - 1;
264        int strIdxStart = 0;
265        int strIdxEnd = strArr.length - 1;
266        char ch;
267
268        boolean containsStar = false;
269        for (char aPatArr : patArr) {
270            if (aPatArr == '*') {
271                containsStar = true;
272                break;
273            }
274        }
275
276        if (!containsStar) {
277            // No '*'s, so we make a shortcut
278            if (patIdxEnd != strIdxEnd) {
279                // Pattern and string do not have the same size
280                return false;
281            }
282            for (int i = 0; i <= patIdxEnd; i++) {
283                ch = patArr[i];
284                if (ch != '?') {
285                    if (ch != strArr[i]) {
286                        // Character mismatch
287                        return false;
288                    }
289                }
290            }
291            // String matches against pattern
292            return true;
293        }
294
295
296        if (patIdxEnd == 0) {
297            // Pattern contains only '*', which matches anything
298            return true;
299        }
300
301        // Process characters before first star
302        while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
303            if (ch != '?') {
304                if (ch != strArr[strIdxStart]) {
305                    // Character mismatch
306                    return false;
307                }
308            }
309            patIdxStart++;
310            strIdxStart++;
311        }
312        if (strIdxStart > strIdxEnd) {
313            // All characters in the string are used. Check if only '*'s are
314            // left in the pattern. If so, we succeeded. Otherwise failure.
315            for (int i = patIdxStart; i <= patIdxEnd; i++) {
316                if (patArr[i] != '*') {
317                    return false;
318                }
319            }
320            return true;
321        }
322
323        // Process characters after last star
324        while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
325            if (ch != '?') {
326                if (ch != strArr[strIdxEnd]) {
327                    // Character mismatch
328                    return false;
329                }
330            }
331            patIdxEnd--;
332            strIdxEnd--;
333        }
334        if (strIdxStart > strIdxEnd) {
335            // All characters in the string are used. Check if only '*'s are
336            // left in the pattern. If so, we succeeded. Otherwise failure.
337            for (int i = patIdxStart; i <= patIdxEnd; i++) {
338                if (patArr[i] != '*') {
339                    return false;
340                }
341            }
342            return true;
343        }
344
345        // process pattern between stars. padIdxStart and patIdxEnd point
346        // always to a '*'.
347        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
348            int patIdxTmp = -1;
349            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
350                if (patArr[i] == '*') {
351                    patIdxTmp = i;
352                    break;
353                }
354            }
355            if (patIdxTmp == patIdxStart + 1) {
356                // Two stars next to each other, skip the first one.
357                patIdxStart++;
358                continue;
359            }
360            // Find the pattern between padIdxStart & padIdxTmp in str between
361            // strIdxStart & strIdxEnd
362            int patLength = (patIdxTmp - patIdxStart - 1);
363            int strLength = (strIdxEnd - strIdxStart + 1);
364            int foundIdx = -1;
365            strLoop:
366            for (int i = 0; i <= strLength - patLength; i++) {
367                for (int j = 0; j < patLength; j++) {
368                    ch = patArr[patIdxStart + j + 1];
369                    if (ch != '?') {
370                        if (ch != strArr[strIdxStart + i + j]) {
371                            continue strLoop;
372                        }
373                    }
374                }
375
376                foundIdx = strIdxStart + i;
377                break;
378            }
379
380            if (foundIdx == -1) {
381                return false;
382            }
383
384            patIdxStart = patIdxTmp;
385            strIdxStart = foundIdx + patLength;
386        }
387
388        // All characters in the string are used. Check if only '*'s are left
389        // in the pattern. If so, we succeeded. Otherwise failure.
390        for (int i = patIdxStart; i <= patIdxEnd; i++) {
391            if (patArr[i] != '*') {
392                return false;
393            }
394        }
395
396        return true;
397    }
398
399    /**
400     * Given a pattern and a full path, determine the pattern-mapped part.
401     * <p>For example:
402     * <ul>
403     * <li>'<code>/docs/cvs/commit.html</code>' and '<code>/docs/cvs/commit.html</code> -> ''</li>
404     * <li>'<code>/docs/*</code>' and '<code>/docs/cvs/commit</code> -> '<code>cvs/commit</code>'</li>
405     * <li>'<code>/docs/cvs/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>commit.html</code>'</li>
406     * <li>'<code>/docs/**</code>' and '<code>/docs/cvs/commit</code> -> '<code>cvs/commit</code>'</li>
407     * <li>'<code>/docs/**\/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>cvs/commit.html</code>'</li>
408     * <li>'<code>/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>docs/cvs/commit.html</code>'</li>
409     * <li>'<code>*.html</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>/docs/cvs/commit.html</code>'</li>
410     * <li>'<code>*</code>' and '<code>/docs/cvs/commit.html</code> -> '<code>/docs/cvs/commit.html</code>'</li>
411     * </ul>
412     * <p>Assumes that {@link #match} returns <code>true</code> for '<code>pattern</code>'
413     * and '<code>path</code>', but does <strong>not</strong> enforce this.
414     */
415    public String extractPathWithinPattern(String pattern, String path) {
416        String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, false, true);
417        String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, false, true);
418        StringBuilder builder = new StringBuilder();
419        boolean pathStarted = false;
420
421        for (int segment = 0; segment < patternParts.length; segment++) {
422            String patternPart = patternParts[segment];
423            if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) {
424                for (; segment < pathParts.length; segment++) {
425                    if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) {
426                        builder.append(this.pathSeparator);
427                    }
428                    builder.append(pathParts[segment]);
429                    pathStarted = true;
430                }
431            }
432        }
433
434        return builder.toString();
435    }
436
437
438}