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    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}