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
018 package org.apache.commons.net.ftp.parser;
019 import java.text.ParseException;
020 import java.text.SimpleDateFormat;
021 import java.util.GregorianCalendar;
022 import java.util.HashMap;
023 import java.util.Locale;
024 import java.util.TimeZone;
025
026 import org.apache.commons.net.ftp.FTPFile;
027 import 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 */
055 public 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 break;
213 // ignore unexpected flag for now.
214 } // switch
215 } // each char
216 }
217
218 public static FTPFile parseEntry(String entry) {
219 return PARSER.parseFTPEntry(entry);
220 }
221
222 public static MLSxEntryParser getInstance() {
223 return PARSER;
224 }
225 }