    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.ParsePosition;
    020import java.text.SimpleDateFormat;
    021import java.util.Calendar;
    022import java.util.Date;
    023import java.util.GregorianCalendar;
    024import java.util.HashMap;
    025import java.util.Locale;
    026import java.util.TimeZone;
    027
    028import org.apache.commons.net.ftp.FTPFile;
    029import org.apache.commons.net.ftp.FTPFileEntryParserImpl;
    030
    031/**
    032 * Parser class for MSLT and MLSD replies. See RFC 3659.
    033 * <p>
    034 * Format is as follows:
    035 * </p>
    036 * <pre>
    037 * entry            = [ facts ] SP pathname
    038 * facts            = 1*( fact ";" )
    039 * fact             = factname "=" value
    040 * factname         = "Size" / "Modify" / "Create" /
    041 *                    "Type" / "Unique" / "Perm" /
    042 *                    "Lang" / "Media-Type" / "CharSet" /
    043 * os-depend-fact / local-fact
    044 * os-depend-fact   = {IANA assigned OS name} "." token
    045 * local-fact       = "X." token
    046 * value            = *SCHAR
    047 *
    048 * Sample os-depend-fact:
    049 * UNIX.group=0;UNIX.mode=0755;UNIX.owner=0;
    050 * </pre>
    051 * <p>
    052 * A single control response entry (MLST) is returned with a leading space;
    053 * multiple (data) entries are returned without any leading spaces.
    054 * The parser requires that the leading space from the MLST entry is removed.
    055 * MLSD entries can begin with a single space if there are no facts.
    056 * </p>
    057 *
    058 * @since 3.0
    059 */
    060public class MLSxEntryParser extends FTPFileEntryParserImpl
    061{
    062    // This class is immutable, so a single instance can be shared.
    063    private static final MLSxEntryParser INSTANCE = new MLSxEntryParser();
    064
    065    private static final HashMap<String, Integer> TYPE_TO_INT = new HashMap<>();
    066    static {
    067        TYPE_TO_INT.put("file", Integer.valueOf(FTPFile.FILE_TYPE));
    068        TYPE_TO_INT.put("cdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // listed directory
    069        TYPE_TO_INT.put("pdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // a parent dir
    070        TYPE_TO_INT.put("dir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // dir or sub-dir
    071    }
    072
    073    private static final int[] UNIX_GROUPS = { // Groups in order of mode digits
    074        FTPFile.USER_ACCESS,
    075        FTPFile.GROUP_ACCESS,
    076        FTPFile.WORLD_ACCESS,
    077    };
    078
    079    private static final int[][] UNIX_PERMS = { // perm bits, broken down by octal int value
    080/* 0 */  {},
    081/* 1 */  {FTPFile.EXECUTE_PERMISSION},
    082/* 2 */  {FTPFile.WRITE_PERMISSION},
    083/* 3 */  {FTPFile.EXECUTE_PERMISSION, FTPFile.WRITE_PERMISSION},
    084/* 4 */  {FTPFile.READ_PERMISSION},
    085/* 5 */  {FTPFile.READ_PERMISSION, FTPFile.EXECUTE_PERMISSION},
    086/* 6 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION},
    087/* 7 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION, FTPFile.EXECUTE_PERMISSION},
    088    };
    089
    090    /**
    091     * Create the parser for MSLT and MSLD listing entries
    092     * This class is immutable, so one can use {@link #getInstance()} instead.
    093     */
    094    public MLSxEntryParser()
    095    {
    096    }
    097
    098    @Override
    099    public FTPFile parseFTPEntry(final String entry) {
    100        if (entry.startsWith(" ")) {// leading space means no facts are present
    101            if (entry.length() > 1) { // is there a path name?
    102                final FTPFile file = new FTPFile();
    103                file.setRawListing(entry);
    104                file.setName(entry.substring(1));
    105                return file;
    106            }
    107            return null; // Invalid - no pathname
    108
    109        }
    110        final String parts[] = entry.split(" ",2); // Path may contain space
    111        if (parts.length != 2 || parts[1].isEmpty()) {
    112            return null; // no space found or no file name
    113        }
    114        final String factList = parts[0];
    115        if (!factList.endsWith(";")) {
    116            return null;
    117        }
    118        final FTPFile file = new FTPFile();
    119        file.setRawListing(entry);
    120        file.setName(parts[1]);
    121        final String[] facts = factList.split(";");
    122        final boolean hasUnixMode = parts[0].toLowerCase(Locale.ENGLISH).contains("unix.mode=");
    123        for(final String fact : facts) {
    124            final String []factparts = fact.split("=", -1); // Don't drop empty values
    125// Sample missing permission
    126// drwx------   2 mirror   mirror       4096 Mar 13  2010 subversion
    127// modify=20100313224553;perm=;type=dir;unique=811U282598;UNIX.group=500;UNIX.mode=0700;UNIX.owner=500; subversion
    128            if (factparts.length != 2) {
    129                return null; // invalid - there was no "=" sign
    130            }
    131            final String factname = factparts[0].toLowerCase(Locale.ENGLISH);
    132            final String factvalue = factparts[1];
    133            if (factvalue.isEmpty()) {
    134                continue; // nothing to see here
    135            }
    136            final String valueLowerCase = factvalue.toLowerCase(Locale.ENGLISH);
    137            if ("size".equals(factname) || "sizd".equals(factname)) {
    138                file.setSize(Long.parseLong(factvalue));
    139            }
    140            else if ("modify".equals(factname)) {
    141                final Calendar parsed = parseGMTdateTime(factvalue);
    142                if (parsed == null) {
    143                    return null;
    144                }
    145                file.setTimestamp(parsed);
    146            }
    147            else if ("type".equals(factname)) {
    148                    final Integer intType = TYPE_TO_INT.get(valueLowerCase);
    149                    if (intType == null) {
    150                        file.setType(FTPFile.UNKNOWN_TYPE);
    151                    } else {
    152                        file.setType(intType.intValue());
    153                    }
    154            }
    155            else if (factname.startsWith("unix.")) {
    156                final String unixfact = factname.substring("unix.".length()).toLowerCase(Locale.ENGLISH);
    157                if ("group".equals(unixfact)){
    158                    file.setGroup(factvalue);
    159                } else if ("owner".equals(unixfact)){
    160                    file.setUser(factvalue);
    161                } else if ("mode".equals(unixfact)){ // e.g. 0[1]755
    162                    final int off = factvalue.length()-3; // only parse last 3 digits
    163                    for(int i=0; i < 3; i++){
    164                        final int ch = factvalue.charAt(off+i)-'0';
    165                        if (ch >= 0 && ch <= 7) { // Check it's valid octal
    166                            for(final int p : UNIX_PERMS[ch]) {
    167                                file.setPermission(UNIX_GROUPS[i], p, true);
    168                            }
    169                        } else {
    170                            // TODO should this cause failure, or can it be reported somehow?
    171                        }
    172                    } // digits
    173                } // mode
    174            } // unix.
    175            else if (!hasUnixMode && "perm".equals(factname)) { // skip if we have the UNIX.mode
    176                doUnixPerms(file, valueLowerCase);
    177            } // process "perm"
    178        } // each fact
    179        return file;
    180    }
    181
    182    /**
    183     * Parse a GMT time stamp of the form YYYYMMDDHHMMSS[.sss]
    184     *
    185     * @param timestamp the date-time to parse
    186     * @return a Calendar entry, may be {@code null}
    187     * @since 3.4
    188     */
    189    public static Calendar parseGMTdateTime(final String timestamp) {
    190        final SimpleDateFormat sdf;
    191        final boolean hasMillis;
    192        if (timestamp.contains(".")){
    193            sdf = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
    194            hasMillis = true;
    195        } else {
    196            sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    197            hasMillis = false;
    198        }
    199        final TimeZone GMT = TimeZone.getTimeZone("GMT");
    200        // both time zones need to be set for the parse to work OK
    201        sdf.setTimeZone(GMT);
    202        final GregorianCalendar gc = new GregorianCalendar(GMT);
    203        final ParsePosition pos = new ParsePosition(0);
    204        sdf.setLenient(false); // We want to parse the whole string
    205        final Date parsed = sdf.parse(timestamp, pos);
    206        if (pos.getIndex()  != timestamp.length()) {
    207            return null; // did not fully parse the input
    208        }
    209        gc.setTime(parsed);
    210        if (!hasMillis) {
    211            gc.clear(Calendar.MILLISECOND); // flag up missing ms units
    212        }
    213        return gc;
    214    }
    215
    216    //              perm-fact    = "Perm" "=" *pvals
    217    //              pvals        = "a" / "c" / "d" / "e" / "f" /
    218    //                             "l" / "m" / "p" / "r" / "w"
    219    private void doUnixPerms(final FTPFile file, final String valueLowerCase) {
    220        for(final char c : valueLowerCase.toCharArray()) {
    221            // TODO these are mostly just guesses at present
    222            switch (c) {
    223                case 'a':     // (file) may APPEnd
    224                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    225                    break;
    226                case 'c':     // (dir) files may be created in the dir
    227                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    228                    break;
    229                case 'd':     // deletable
    230                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    231                    break;
    232                case 'e':     // (dir) can change to this dir
    233                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
    234                    break;
    235                case 'f':     // (file) renamable
    236                    // ?? file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    237                    break;
    238                case 'l':     // (dir) can be listed
    239                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true);
    240                    break;
    241                case 'm':     // (dir) can create directory here
    242                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    243                    break;
    244                case 'p':     // (dir) entries may be deleted
    245                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    246                    break;
    247                case 'r':     // (files) file may be RETRieved
    248                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
    249                    break;
    250                case 'w':     // (files) file may be STORed
    251                    file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
    252                    break;
    253                default:
    254                    break;
    255                    // ignore unexpected flag for now.
    256            } // switch
    257        } // each char
    258    }
    259
    260    public static FTPFile parseEntry(final String entry) {
    261        return INSTANCE.parseFTPEntry(entry);
    262    }
    263
    264    public static  MLSxEntryParser getInstance() {
    265        return INSTANCE;
    266    }
    267}