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