    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017
    018    package org.apache.commons.net.ftp.parser;
    019    import java.text.ParseException;
    020    import java.text.SimpleDateFormat;
    021    import java.util.GregorianCalendar;
    022    import java.util.HashMap;
    023    import java.util.Locale;
    024    import java.util.TimeZone;
    025
    026    import org.apache.commons.net.ftp.FTPFile;
    027    import org.apache.commons.net.ftp.FTPFileEntryParserImpl;
    028
    029    /**
    030     * Parser class for MSLT and MLSD replies. See RFC 3659.
    031     * <p>
    032     * Format is as follows:
    033     * <pre>
    034     * entry            = [ facts ] SP pathname
    035     * facts            = 1*( fact ";" )
    036     * fact             = factname "=" value
    037     * factname         = "Size" / "Modify" / "Create" /
    038     *                    "Type" / "Unique" / "Perm" /
    039     *                    "Lang" / "Media-Type" / "CharSet" /
    040     * os-depend-fact / local-fact
    041     * os-depend-fact   = <IANA assigned OS name> "." token
    042     * local-fact       = "X." token
    043     * value            = *SCHAR
    044     *
    045     * Sample os-depend-fact:
    046     * UNIX.group=0;UNIX.mode=0755;UNIX.owner=0;
    047     * </pre>
    048     * A single control response entry (MLST) is returned with a leading space;
    049     * multiple (data) entries are returned without any leading spaces.
    050     * The parser requires that the leading space from the MLST entry is removed.
    051     * MLSD entries can begin with a single space if there are no facts.
    052     *
    053     * @since 3.0
    054     */
    055    public class MLSxEntryParser extends FTPFileEntryParserImpl
    056    {
    057        // This class is immutable, so a single instance can be shared.
    058        private static final MLSxEntryParser PARSER = new MLSxEntryParser();
    059
    060        private static final HashMap<String, Integer> TYPE_TO_INT = new HashMap<String, Integer>();
    061        static {
    062            TYPE_TO_INT.put("file", Integer.valueOf(FTPFile.FILE_TYPE));
    063            TYPE_TO_INT.put("cdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // listed directory
    064            TYPE_TO_INT.put("pdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // a parent dir
    065            TYPE_TO_INT.put("dir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // dir or sub-dir
    066        }
    067
    068        private static int UNIX_GROUPS[] = { // Groups in order of mode digits
    069            FTPFile.USER_ACCESS,
    070            FTPFile.GROUP_ACCESS,
    071            FTPFile.WORLD_ACCESS,
    072        };
    073
    074        private static int UNIX_PERMS[][] = { // perm bits, broken down by octal int value
    075    /* 0 */  {},
    076    /* 1 */  {FTPFile.EXECUTE_PERMISSION},
    077    /* 2 */  {FTPFile.WRITE_PERMISSION},
    078    /* 3 */  {FTPFile.EXECUTE_PERMISSION, FTPFile.WRITE_PERMISSION},
    079    /* 4 */  {FTPFile.READ_PERMISSION},
    080    /* 5 */  {FTPFile.READ_PERMISSION, FTPFile.EXECUTE_PERMISSION},
    081    /* 6 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION},
    082    /* 7 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION, FTPFile.EXECUTE_PERMISSION},
    083        };
    084
    085        /**
    086         * Create the parser for MSLT and MSLD listing entries
    087         * This class is immutable, so one can use {@link #getInstance()} instead.
    088         */
    089        public MLSxEntryParser()
    090        {
    091            super();
    092        }
    093
    094        public FTPFile parseFTPEntry(String entry) {
    095            String parts[] = entry.split(" ",2); // Path may contain space
    096            if (parts.length != 2) {
    097                return null;
    098            }
    099            FTPFile file = new FTPFile();
    100            file.setRawListing(entry);
    101            file.setName(parts[1]);
    102            String[] facts = parts[0].split(";");
    103            boolean hasUnixMode = parts[0].toLowerCase(Locale.ENGLISH).contains("unix.mode=");
    104            for(String fact : facts) {
    105                String []factparts = fact.split("=");
    106    // Sample missing permission
    107    // drwx------   2 mirror   mirror       4096 Mar 13  2010 subversion
    108    // modify=20100313224553;perm=;type=dir;unique=811U282598;UNIX.group=500;UNIX.mode=0700;UNIX.owner=500; subversion
    109                if (factparts.length != 2) {
    110                    continue; // nothing to do here
    111                }
    112                String factname = factparts[0].toLowerCase(Locale.ENGLISH);
    113                String factvalue = factparts[1];
    114                String valueLowerCase = factvalue.toLowerCase(Locale.ENGLISH);
    115                if ("size".equals(factname)) {
    116                    file.setSize(Long.parseLong(factvalue));
    117                }
    118                else if ("sizd".equals(factname)) { // Directory size
    119                    file.setSize(Long.parseLong(factvalue));
    120                }
    121                else if ("modify".equals(factname)) {
    122                    // YYYYMMDDHHMMSS[.sss]
    123                    SimpleDateFormat sdf; // Not thread-safe
    124                    if (factvalue.contains(".")){
    125                        sdf = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
    126                    } else {
    127                        sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    128                    }
    129                    TimeZone GMT = TimeZone.getTimeZone("GMT"); // both need to be set for the parse to work OK
    130                    sdf.setTimeZone(GMT);
    131                    GregorianCalendar gc = new GregorianCalendar(GMT);
    132                    try {
    133                        gc.setTime(sdf.parse(factvalue));
    134                    } catch (ParseException e) {
    135                        // TODO ??
    136                    }
    137                    file.setTimestamp(gc);
    138                }
    139                else if ("type".equals(factname)) {
    140                        Integer intType = TYPE_TO_INT.get(valueLowerCase);
    141                        if (intType == null) {
    142                            file.setType(FTPFile.UNKNOWN_TYPE);
    143                        } else {
    144                            file.setType(intType.intValue());
    145                        }
    146                }
    147                else if (factname.startsWith("unix.")) {
    148                    String unixfact = factname.substring("unix.".length()).toLowerCase(Locale.ENGLISH);
    149                    if ("group".equals(unixfact)){
    150                        file.setGroup(factvalue);
    151                    } else if ("owner".equals(unixfact)){
    152                        file.setUser(factvalue);
    153                    } else if ("mode".equals(unixfact)){ // e.g. 0[1]755
    154                        int off = factvalue.length()-3; // only parse last 3 digits
    155                        for(int i=0; i < 3; i++){
    156                            int ch = factvalue.charAt(off+i)-'0';
    157                            if (ch >= 0 && ch <= 7) { // Check it's valid octal
    158                                for(int p : UNIX_PERMS[ch]) {
    159                                    file.setPermission(UNIX_GROUPS[i], p, true);
    160                                }
    161                            } else {
    162                                // TODO should this cause failure, or can it be reported somehow?
    163                            }
    164                        } // digits
    165                    } // mode
    166                } // unix.
    167                else if (!hasUnixMode && "perm".equals(factname)) { // skip if we have the UNIX.mode
    168                    doUnixPerms(file, valueLowerCase);
    169                } // process "perm"
    170            } // each fact
    171            return file;
    172        }
    173
    174        //              perm-fact    = "Perm" "=" *pvals
    175        //              pvals        = "a" / "c" / "d" / "e" / "f" /
    176        //                             "l" / "m" / "p" / "r" / "w"
    177        private void doUnixPerms(FTPFile file, String valueLowerCase) {
    178            for(char c : valueLowerCase.toCharArray()) {
    179                // TODO these are mostly just guesses at present
    180                switch (c) {
    181                    case 'a':     // (file) may APPEnd
    182                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    183                        break;
    184                    case 'c':     // (dir) files may be created in the dir
    185                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    186                        break;
    187                    case 'd':     // deletable
    188                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    189                        break;
    190                    case 'e':     // (dir) can change to this dir
    191                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
    192                        break;
    193                    case 'f':     // (file) renamable
    194                        // ?? file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    195                        break;
    196                    case 'l':     // (dir) can be listed
    197                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true);
    198                        break;
    199                    case 'm':     // (dir) can create directory here
    200                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    201                        break;
    202                    case 'p':     // (dir) entries may be deleted
    203                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    204                        break;
    205                    case 'r':     // (files) file may be RETRieved
    206                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
    207                        break;
    208                    case 'w':     // (files) file may be STORed
    209                        file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    210                        break;
    211                    default:
    212                        // ignore unexpected flag for now.
    213                } // switch
    214            } // each char
    215        }
    216
    217        public static FTPFile parseEntry(String entry) {
    218            return PARSER.parseFTPEntry(entry);
    219        }
    220
    221        public static  MLSxEntryParser getInstance() {
    222            return PARSER;
    223        }
    224    }