001/*
002 *  Copyright 2020 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.util;
017
018import java.nio.charset.StandardCharsets;
019import java.util.function.Predicate;
020
021/**
022 * Utility class for encoding file names and paths.<br>
023 * These methods are used to provide safe names to be stored on disk.<br>
024 * The encode* methods %-encode all reserved chars, plus the '%' char itself.<br>
025 * The filter* methods replace reserved chars with the '_' char.<br><br>
026 * Reserved chars are <pre>\ / ? : * | " &lt; &gt;</pre>
027 */
028public final class FilenameUtils
029{
030    /**
031     * Character used to replace reserved chars in the filter* methods.
032     */
033    public static final char REPLACEMENT_CHAR = '_';
034    
035    private static Predicate<Character> _isReserved = c -> ';' == c || '\\' == c || '/' == c || '?' == c || ':' == c || '*' == c || '|' == c || '"' == c || '<' == c || '>' == c;
036    
037    private FilenameUtils()
038    {
039        // empty
040    }
041    
042    /**
043     * %-encode a file path to be stored on disk.
044     * @param path the file path to encode
045     * @return the encoded file path.
046     */
047    public static String encodePath(String path)
048    {
049        return _encodePath(path, Predicate.not(_isReserved.or(c -> '%' == c)).or(c -> '/' == c), true);
050    }
051    
052    /**
053     * %-encode a file name to be stored on disk.
054     * @param name the file name to encode
055     * @return the encoded file name.
056     */
057    public static String encodeName(String name)
058    {
059        return _encodePath(name, Predicate.not(_isReserved.or(c -> '%' == c)), true);
060    }
061    
062    /**
063     * Replace reserved characters from a file path with a _.
064     * @param path the file path to transform
065     * @return the filtered file path
066     */
067    public static String filterPath(String path)
068    {
069        return _encodePath(path, Predicate.not(_isReserved).or(c -> '/' == c), false);
070    }
071    
072    /**
073     * Replace reserved characters from a file name with a _.
074     * @param name the file name to transform
075     * @return the filtered file name
076     */
077    public static String filterName(String name)
078    {
079        return _encodePath(name, Predicate.not(_isReserved), false);
080    }
081    
082    @SuppressWarnings("all")
083    // very similar to URIUtils#encode but this one allows UTF-8 chars and not only ASCII bytes
084    private static String _encodePath(String path, Predicate<Character> charactersToKeep, boolean encode)
085    {
086        StringBuilder result = new StringBuilder();
087        
088        char[] chars = path.toCharArray();
089        
090        for (char c : chars)
091        {
092            if (charactersToKeep.test(c)) 
093            {
094                result.append(c);
095            }
096            else if (encode)
097            {
098                byte[] bytes = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
099                for (byte b : bytes) 
100                {
101                    if (b < 0) 
102                    {
103                        b += 256;
104                    }
105                    
106                    result.append('%');
107                    char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16));
108                    char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16));
109                    result.append(hex1);
110                    result.append(hex2);
111                }
112            }
113            else
114            {
115                result.append(REPLACEMENT_CHAR);
116            }
117        }
118        
119        return result.toString();
120    }
121    
122    /**
123     * Decodes an URI-encoded String.
124     * @param source the String to decode.
125     * @return the decoded String.
126     */
127    @SuppressWarnings("all")
128    public static String decode(String source)
129    {
130        return URIUtils.decode(source);
131    }
132}