    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
    018package org.apache.commons.net.ftp.parser;
    019import java.text.ParseException;
    020import java.text.SimpleDateFormat;
    021import java.util.GregorianCalendar;
    022import java.util.HashMap;
    023import java.util.Locale;
    024import java.util.TimeZone;
    025
    026import org.apache.commons.net.ftp.FTPFile;
    027import 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 */
    055public 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//    @Override
    095    public FTPFile parseFTPEntry(String entry) {
    096        String parts[] = entry.split(" ",2); // Path may contain space
    097        if (parts.length != 2) {
    098            return null;
    099        }
    100        FTPFile file = new FTPFile();
    101        file.setRawListing(entry);
    102        file.setName(parts[1]);
    103        String[] facts = parts[0].split(";");
    104        boolean hasUnixMode = parts[0].toLowerCase(Locale.ENGLISH).contains("unix.mode=");
    105        for(String fact : facts) {
    106            String []factparts = fact.split("=");
    107// Sample missing permission
    108// drwx------   2 mirror   mirror       4096 Mar 13  2010 subversion
    109// modify=20100313224553;perm=;type=dir;unique=811U282598;UNIX.group=500;UNIX.mode=0700;UNIX.owner=500; subversion
    110            if (factparts.length != 2) {
    111                continue; // nothing to do here
    112            }
    113            String factname = factparts[0].toLowerCase(Locale.ENGLISH);
    114            String factvalue = factparts[1];
    115            String valueLowerCase = factvalue.toLowerCase(Locale.ENGLISH);
    116            if ("size".equals(factname)) {
    117                file.setSize(Long.parseLong(factvalue));
    118            }
    119            else if ("sizd".equals(factname)) { // Directory size
    120                file.setSize(Long.parseLong(factvalue));
    121            }
    122            else if ("modify".equals(factname)) {
    123                // YYYYMMDDHHMMSS[.sss]
    124                SimpleDateFormat sdf; // Not thread-safe
    125                if (factvalue.contains(".")){
    126                    sdf = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
    127                } else {
    128                    sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    129                }
    130                TimeZone GMT = TimeZone.getTimeZone("GMT"); // both need to be set for the parse to work OK
    131                sdf.setTimeZone(GMT);
    132                GregorianCalendar gc = new GregorianCalendar(GMT);
    133                try {
    134                    gc.setTime(sdf.parse(factvalue));
    135                } catch (ParseException e) {
    136                    // TODO ??
    137                }
    138                file.setTimestamp(gc);
    139            }
    140            else if ("type".equals(factname)) {
    141                    Integer intType = TYPE_TO_INT.get(valueLowerCase);
    142                    if (intType == null) {
    143                        file.setType(FTPFile.UNKNOWN_TYPE);
    144                    } else {
    145                        file.setType(intType.intValue());
    146                    }
    147            }
    148            else if (factname.startsWith("unix.")) {
    149                String unixfact = factname.substring("unix.".length()).toLowerCase(Locale.ENGLISH);
    150                if ("group".equals(unixfact)){
    151                    file.setGroup(factvalue);
    152                } else if ("owner".equals(unixfact)){
    153                    file.setUser(factvalue);
    154                } else if ("mode".equals(unixfact)){ // e.g. 0[1]755
    155                    int off = factvalue.length()-3; // only parse last 3 digits
    156                    for(int i=0; i < 3; i++){
    157                        int ch = factvalue.charAt(off+i)-'0';
    158                        if (ch >= 0 && ch <= 7) { // Check it's valid octal
    159                            for(int p : UNIX_PERMS[ch]) {
    160                                file.setPermission(UNIX_GROUPS[i], p, true);
    161                            }
    162                        } else {
    163                            // TODO should this cause failure, or can it be reported somehow?
    164                        }
    165                    } // digits
    166                } // mode
    167            } // unix.
    168            else if (!hasUnixMode && "perm".equals(factname)) { // skip if we have the UNIX.mode
    169                doUnixPerms(file, valueLowerCase);
    170            } // process "perm"
    171        } // each fact
    172        return file;
    173    }
    174
    175    //              perm-fact    = "Perm" "=" *pvals
    176    //              pvals        = "a" / "c" / "d" / "e" / "f" /
    177    //                             "l" / "m" / "p" / "r" / "w"
    178    private void doUnixPerms(FTPFile file, String valueLowerCase) {
    179        for(char c : valueLowerCase.toCharArray()) {
    180            // TODO these are mostly just guesses at present
    181            switch (c) {
    182                case 'a':     // (file) may APPEnd
    183                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    184                    break;
    185                case 'c':     // (dir) files may be created in the dir
    186                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    187                    break;
    188                case 'd':     // deletable
    189                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    190                    break;
    191                case 'e':     // (dir) can change to this dir
    192                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
    193                    break;
    194                case 'f':     // (file) renamable
    195                    // ?? file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    196                    break;
    197                case 'l':     // (dir) can be listed
    198                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true);
    199                    break;
    200                case 'm':     // (dir) can create directory here
    201                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    202                    break;
    203                case 'p':     // (dir) entries may be deleted
    204                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    205                    break;
    206                case 'r':     // (files) file may be RETRieved
    207                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
    208                    break;
    209                case 'w':     // (files) file may be STORed
    210                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    211                    break;
    212                default:
    213                    break;
    214                    // ignore unexpected flag for now.
    215            } // switch
    216        } // each char
    217    }
    218
    219    public static FTPFile parseEntry(String entry) {
    220        return PARSER.parseFTPEntry(entry);
    221    }
    222
    223    public static  MLSxEntryParser getInstance() {
    224        return PARSER;
    225    }
    226}