View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.ftp.parser;
19  import java.text.ParsePosition;
20  import java.text.SimpleDateFormat;
21  import java.util.Calendar;
22  import java.util.Date;
23  import java.util.GregorianCalendar;
24  import java.util.HashMap;
25  import java.util.Locale;
26  import java.util.TimeZone;
27  
28  import org.apache.commons.net.ftp.FTPFile;
29  import org.apache.commons.net.ftp.FTPFileEntryParserImpl;
30  
31  /**
32   * Parser class for MSLT and MLSD replies. See RFC 3659.
33   * <p>
34   * Format is as follows:
35   * <pre>
36   * entry            = [ facts ] SP pathname
37   * facts            = 1*( fact ";" )
38   * fact             = factname "=" value
39   * factname         = "Size" / "Modify" / "Create" /
40   *                    "Type" / "Unique" / "Perm" /
41   *                    "Lang" / "Media-Type" / "CharSet" /
42   * os-depend-fact / local-fact
43   * os-depend-fact   = {IANA assigned OS name} "." token
44   * local-fact       = "X." token
45   * value            = *SCHAR
46   *
47   * Sample os-depend-fact:
48   * UNIX.group=0;UNIX.mode=0755;UNIX.owner=0;
49   * </pre>
50   * A single control response entry (MLST) is returned with a leading space;
51   * multiple (data) entries are returned without any leading spaces.
52   * The parser requires that the leading space from the MLST entry is removed.
53   * MLSD entries can begin with a single space if there are no facts.
54   *
55   * @since 3.0
56   */
57  public class MLSxEntryParser extends FTPFileEntryParserImpl
58  {
59      // This class is immutable, so a single instance can be shared.
60      private static final MLSxEntryParser PARSER = new MLSxEntryParser();
61  
62      private static final HashMap<String, Integer> TYPE_TO_INT = new HashMap<String, Integer>();
63      static {
64          TYPE_TO_INT.put("file", Integer.valueOf(FTPFile.FILE_TYPE));
65          TYPE_TO_INT.put("cdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // listed directory
66          TYPE_TO_INT.put("pdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // a parent dir
67          TYPE_TO_INT.put("dir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // dir or sub-dir
68      }
69  
70      private static int UNIX_GROUPS[] = { // Groups in order of mode digits
71          FTPFile.USER_ACCESS,
72          FTPFile.GROUP_ACCESS,
73          FTPFile.WORLD_ACCESS,
74      };
75  
76      private static int UNIX_PERMS[][] = { // perm bits, broken down by octal int value
77  /* 0 */  {},
78  /* 1 */  {FTPFile.EXECUTE_PERMISSION},
79  /* 2 */  {FTPFile.WRITE_PERMISSION},
80  /* 3 */  {FTPFile.EXECUTE_PERMISSION, FTPFile.WRITE_PERMISSION},
81  /* 4 */  {FTPFile.READ_PERMISSION},
82  /* 5 */  {FTPFile.READ_PERMISSION, FTPFile.EXECUTE_PERMISSION},
83  /* 6 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION},
84  /* 7 */  {FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION, FTPFile.EXECUTE_PERMISSION},
85      };
86  
87      /**
88       * Create the parser for MSLT and MSLD listing entries
89       * This class is immutable, so one can use {@link #getInstance()} instead.
90       */
91      public MLSxEntryParser()
92      {
93          super();
94      }
95  
96      @Override
97      public FTPFile parseFTPEntry(String entry) {
98          if (entry.startsWith(" ")) {// leading space means no facts are present
99              if (entry.length() > 1) { // is there a path name?
100                 FTPFile file = new FTPFile();
101                 file.setRawListing(entry);
102                 file.setName(entry.substring(1));
103                 return file;
104             } else {
105                 return null; // Invalid - no pathname
106             }
107 
108         }
109         String parts[] = entry.split(" ",2); // Path may contain space
110         if (parts.length != 2 || parts[1].length() == 0) {
111             return null; // no space found or no file name
112         }
113         final String factList = parts[0];
114         if (!factList.endsWith(";")) {
115             return null;
116         }
117         FTPFile file = new FTPFile();
118         file.setRawListing(entry);
119         file.setName(parts[1]);
120         String[] facts = factList.split(";");
121         boolean hasUnixMode = parts[0].toLowerCase(Locale.ENGLISH).contains("unix.mode=");
122         for(String fact : facts) {
123             String []factparts = fact.split("=", -1); // Don't drop empty values
124 // Sample missing permission
125 // drwx------   2 mirror   mirror       4096 Mar 13  2010 subversion
126 // modify=20100313224553;perm=;type=dir;unique=811U282598;UNIX.group=500;UNIX.mode=0700;UNIX.owner=500; subversion
127             if (factparts.length != 2) {
128                 return null; // invalid - there was no "=" sign
129             }
130             String factname = factparts[0].toLowerCase(Locale.ENGLISH);
131             String factvalue = factparts[1];
132             if (factvalue.length() == 0) {
133                 continue; // nothing to see here
134             }
135             String valueLowerCase = factvalue.toLowerCase(Locale.ENGLISH);
136             if ("size".equals(factname)) {
137                 file.setSize(Long.parseLong(factvalue));
138             }
139             else if ("sizd".equals(factname)) { // Directory size
140                 file.setSize(Long.parseLong(factvalue));
141             }
142             else if ("modify".equals(factname)) {
143                 final Calendar parsed = parseGMTdateTime(factvalue);
144                 if (parsed == null) {
145                     return null;
146                 }
147                 file.setTimestamp(parsed);
148             }
149             else if ("type".equals(factname)) {
150                     Integer intType = TYPE_TO_INT.get(valueLowerCase);
151                     if (intType == null) {
152                         file.setType(FTPFile.UNKNOWN_TYPE);
153                     } else {
154                         file.setType(intType.intValue());
155                     }
156             }
157             else if (factname.startsWith("unix.")) {
158                 String unixfact = factname.substring("unix.".length()).toLowerCase(Locale.ENGLISH);
159                 if ("group".equals(unixfact)){
160                     file.setGroup(factvalue);
161                 } else if ("owner".equals(unixfact)){
162                     file.setUser(factvalue);
163                 } else if ("mode".equals(unixfact)){ // e.g. 0[1]755
164                     int off = factvalue.length()-3; // only parse last 3 digits
165                     for(int i=0; i < 3; i++){
166                         int ch = factvalue.charAt(off+i)-'0';
167                         if (ch >= 0 && ch <= 7) { // Check it's valid octal
168                             for(int p : UNIX_PERMS[ch]) {
169                                 file.setPermission(UNIX_GROUPS[i], p, true);
170                             }
171                         } else {
172                             // TODO should this cause failure, or can it be reported somehow?
173                         }
174                     } // digits
175                 } // mode
176             } // unix.
177             else if (!hasUnixMode && "perm".equals(factname)) { // skip if we have the UNIX.mode
178                 doUnixPerms(file, valueLowerCase);
179             } // process "perm"
180         } // each fact
181         return file;
182     }
183 
184     /**
185      * Parse a GMT time stamp of the form YYYYMMDDHHMMSS[.sss]
186      *
187      * @param timestamp the date-time to parse
188      * @return a Calendar entry, may be {@code null}
189      * @since 3.4
190      */
191     public static Calendar parseGMTdateTime(String timestamp) {
192         final SimpleDateFormat sdf;
193         final boolean hasMillis;
194         if (timestamp.contains(".")){
195             sdf = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
196             hasMillis = true;
197         } else {
198             sdf = new SimpleDateFormat("yyyyMMddHHmmss");
199             hasMillis = false;
200         }
201         TimeZone GMT = TimeZone.getTimeZone("GMT");
202         // both timezones need to be set for the parse to work OK
203         sdf.setTimeZone(GMT);
204         GregorianCalendar gc = new GregorianCalendar(GMT);
205         ParsePosition pos = new ParsePosition(0);
206         sdf.setLenient(false); // We want to parse the whole string
207         final Date parsed = sdf.parse(timestamp, pos);
208         if (pos.getIndex()  != timestamp.length()) {
209             return null; // did not fully parse the input
210         }
211         gc.setTime(parsed);
212         if (!hasMillis) {
213             gc.clear(Calendar.MILLISECOND); // flag up missing ms units
214         }
215         return gc;
216     }
217 
218     //              perm-fact    = "Perm" "=" *pvals
219     //              pvals        = "a" / "c" / "d" / "e" / "f" /
220     //                             "l" / "m" / "p" / "r" / "w"
221     private void doUnixPerms(FTPFile file, String valueLowerCase) {
222         for(char c : valueLowerCase.toCharArray()) {
223             // TODO these are mostly just guesses at present
224             switch (c) {
225                 case 'a':     // (file) may APPEnd
226                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
227                     break;
228                 case 'c':     // (dir) files may be created in the dir
229                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
230                     break;
231                 case 'd':     // deletable
232                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
233                     break;
234                 case 'e':     // (dir) can change to this dir
235                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
236                     break;
237                 case 'f':     // (file) renamable
238                     // ?? file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
239                     break;
240                 case 'l':     // (dir) can be listed
241                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true);
242                     break;
243                 case 'm':     // (dir) can create directory here
244                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
245                     break;
246                 case 'p':     // (dir) entries may be deleted
247                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
248                     break;
249                 case 'r':     // (files) file may be RETRieved
250                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
251                     break;
252                 case 'w':     // (files) file may be STORed
253                     file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
254                     break;
255                 default:
256                     break;
257                     // ignore unexpected flag for now.
258             } // switch
259         } // each char
260     }
261 
262     public static FTPFile parseEntry(String entry) {
263         return PARSER.parseFTPEntry(entry);
264     }
265 
266     public static  MLSxEntryParser getInstance() {
267         return PARSER;
268     }
269 }