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 * https://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
20 import java.text.ParsePosition;
21 import java.text.SimpleDateFormat;
22 import java.time.Instant;
23 import java.util.Calendar;
24 import java.util.Date;
25 import java.util.GregorianCalendar;
26 import java.util.HashMap;
27 import java.util.Locale;
28 import java.util.TimeZone;
29
30 import org.apache.commons.net.ftp.FTPFile;
31 import org.apache.commons.net.ftp.FTPFileEntryParserImpl;
32
33 /**
34 * Parses {@code MSLT} and {@code MLSD} replies. See <a href="https://datatracker.ietf.org/doc/html/rfc3659">RFC 3659</a>.
35 * <p>
36 * The format is as follows:
37 * </p>
38 *
39 * <pre>
40 * entry = [ facts ] SP path
41 * facts = 1*( fact ";" )
42 * fact = factname "=" value
43 * factname = "Size" / "Modify" / "Create" /
44 * "Type" / "Unique" / "Perm" /
45 * "Lang" / "Media-Type" / "CharSet" /
46 * os-depend-fact / local-fact
47 * os-depend-fact = {IANA assigned OS name} "." token
48 * local-fact = "X." token
49 * value = *SCHAR
50 *
51 * Sample os-depend-fact:
52 * UNIX.group=0;UNIX.mode=0755;UNIX.owner=0;
53 * </pre>
54 * <p>
55 * A single control response entry (MLST) is returned with a leading space; multiple (data) entries are returned without any leading spaces. The parser requires
56 * that the leading space from the MLST entry is removed. MLSD entries can begin with a single space if there are no facts.
57 * </p>
58 *
59 * @since 3.0
60 */
61 public class MLSxEntryParser extends FTPFileEntryParserImpl {
62 // This class is immutable, so a single instance can be shared.
63 private static final MLSxEntryParser INSTANCE = new MLSxEntryParser();
64
65 private static final HashMap<String, Integer> TYPE_TO_INT = new HashMap<>();
66 static {
67 TYPE_TO_INT.put("file", Integer.valueOf(FTPFile.FILE_TYPE));
68 TYPE_TO_INT.put("cdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // listed directory
69 TYPE_TO_INT.put("pdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // a parent dir
70 TYPE_TO_INT.put("dir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // dir or sub-dir
71 }
72
73 private static final int[] UNIX_GROUPS = { // Groups in order of mode digits
74 FTPFile.USER_ACCESS, FTPFile.GROUP_ACCESS, FTPFile.WORLD_ACCESS, };
75
76 private static final int[][] UNIX_PERMS = { // perm bits, broken down by octal int value
77 /* 0 */ {}, /* 1 */ { FTPFile.EXECUTE_PERMISSION }, /* 2 */ { FTPFile.WRITE_PERMISSION },
78 /* 3 */ { FTPFile.EXECUTE_PERMISSION, FTPFile.WRITE_PERMISSION }, /* 4 */ { FTPFile.READ_PERMISSION },
79 /* 5 */ { FTPFile.READ_PERMISSION, FTPFile.EXECUTE_PERMISSION }, /* 6 */ { FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION },
80 /* 7 */ { FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION, FTPFile.EXECUTE_PERMISSION }, };
81
82 /**
83 * Gets the singleton instance.
84 *
85 * @return the singleton instance.
86 */
87 public static MLSxEntryParser getInstance() {
88 return INSTANCE;
89 }
90
91 /**
92 * Parses a line of an FTP server file listing and converts it into a usable format in the form of an {@code FTPFile} instance. If the file listing
93 * line doesn't describe a file, {@code null} should be returned, otherwise a {@code FTPFile} instance representing the files in the directory
94 * is returned.
95 *
96 * @param entry A line of text from the file listing
97 * @return An FTPFile instance corresponding to the supplied entry
98 */
99 public static FTPFile parseEntry(final String entry) {
100 return INSTANCE.parseFTPEntry(entry);
101 }
102
103 /**
104 * Parse a GMT time stamp of the form yyyyMMDDHHMMSS[.sss]
105 *
106 * @param timestamp the date-time to parse
107 * @return a Calendar entry, may be {@code null}
108 * @since 3.4
109 */
110 public static Calendar parseGMTdateTime(final String timestamp) {
111 final SimpleDateFormat dateFormat;
112 final boolean hasMillis;
113 if (timestamp.contains(".")) {
114 dateFormat = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
115 hasMillis = true;
116 } else {
117 dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
118 hasMillis = false;
119 }
120 final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
121 // both time zones need to be set for the parse to work OK
122 dateFormat.setTimeZone(gmtTimeZone);
123 final GregorianCalendar gCalendar = new GregorianCalendar(gmtTimeZone);
124 final ParsePosition pos = new ParsePosition(0);
125 dateFormat.setLenient(false); // We want to parse the whole string
126 final Date parsed = dateFormat.parse(timestamp, pos);
127 if (pos.getIndex() != timestamp.length()) {
128 return null; // did not fully parse the input
129 }
130 gCalendar.setTime(parsed);
131 if (!hasMillis) {
132 gCalendar.clear(Calendar.MILLISECOND); // flag up missing ms units
133 }
134 return gCalendar;
135 }
136
137 /**
138 * Parse a GMT time stamp of the form yyyyMMDDHHMMSS[.sss]
139 *
140 * @param timestamp the date-time to parse
141 * @return a Calendar entry, may be {@code null}
142 * @since 3.9.0
143 */
144 public static Instant parseGmtInstant(final String timestamp) {
145 return parseGMTdateTime(timestamp).toInstant();
146 }
147
148 /**
149 * Creates the parser for MSLT and MSLD listing entries This class is immutable, so one can use {@link #getInstance()} instead.
150 *
151 * @deprecated Use {@link #getInstance()}.
152 */
153 @Deprecated
154 public MLSxEntryParser() {
155 // empty
156 }
157
158 // perm-fact = "Perm" "=" *pvals
159 // pvals = "a" / "c" / "d" / "e" / "f" /
160 // "l" / "m" / "p" / "r" / "w"
161 private void doUnixPerms(final FTPFile file, final String valueLowerCase) {
162 for (final char c : valueLowerCase.toCharArray()) {
163 // TODO these are mostly just guesses at present
164 switch (c) {
165 case 'a': // (file) may APPEnd
166 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
167 break;
168 case 'c': // (dir) files may be created in the dir
169 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
170 break;
171 case 'd': // deletable
172 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
173 break;
174 case 'e': // (dir) can change to this dir
175 file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
176 break;
177 case 'f': // (file) renamable
178 // ?? file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
179 break;
180 case 'l': // (dir) can be listed
181 file.setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true);
182 break;
183 case 'm': // (dir) can create directory here
184 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
185 break;
186 case 'p': // (dir) entries may be deleted
187 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
188 break;
189 case 'r': // (files) file may be RETRieved
190 file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
191 break;
192 case 'w': // (files) file may be STORed
193 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
194 break;
195 default:
196 break;
197 // ignore unexpected flag for now.
198 } // switch
199 } // each char
200 }
201
202 @Override
203 public FTPFile parseFTPEntry(final String entry) {
204 if (entry.startsWith(" ")) { // leading space means no facts are present
205 if (entry.length() > 1) { // is there a path name?
206 final FTPFile file = new FTPFile();
207 file.setRawListing(entry);
208 file.setName(entry.substring(1));
209 return file;
210 }
211 return null; // Invalid - no path
212
213 }
214 final String[] parts = entry.split(" ", 2); // Path may contain space
215 if (parts.length != 2 || parts[1].isEmpty()) {
216 return null; // no space found or no file name
217 }
218 final String factList = parts[0];
219 if (!factList.endsWith(";")) {
220 return null;
221 }
222 final FTPFile file = new FTPFile();
223 file.setRawListing(entry);
224 file.setName(parts[1]);
225 final String[] facts = factList.split(";");
226 final boolean hasUnixMode = parts[0].toLowerCase(Locale.ENGLISH).contains("unix.mode=");
227 for (final String fact : facts) {
228 final String[] factparts = fact.split("=", -1); // Don't drop empty values
229 // Sample missing permission
230 // drwx------ 2 mirror mirror 4096 Mar 13 2010 subversion
231 // modify=20100313224553;perm=;type=dir;unique=811U282598;UNIX.group=500;UNIX.mode=0700;UNIX.owner=500; subversion
232 if (factparts.length != 2) {
233 return null; // invalid - there was no "=" sign
234 }
235 final String factname = factparts[0].toLowerCase(Locale.ENGLISH);
236 final String factvalue = factparts[1];
237 if (factvalue.isEmpty()) {
238 continue; // nothing to see here
239 }
240 final String valueLowerCase = factvalue.toLowerCase(Locale.ENGLISH);
241 switch (factname) {
242 case "size":
243 case "sizd":
244 file.setSize(Long.parseLong(factvalue));
245 break;
246 case "modify": {
247 final Calendar parsed = parseGMTdateTime(factvalue);
248 if (parsed == null) {
249 return null;
250 }
251 file.setTimestamp(parsed);
252 break;
253 }
254 case "type": {
255 final Integer intType = TYPE_TO_INT.get(valueLowerCase);
256 if (intType == null) {
257 file.setType(FTPFile.UNKNOWN_TYPE);
258 } else {
259 file.setType(intType.intValue());
260 }
261 break;
262 }
263 default:
264 if (factname.startsWith("unix.")) {
265 final String unixfact = factname.substring("unix.".length()).toLowerCase(Locale.ENGLISH);
266 switch (unixfact) {
267 case "group":
268 file.setGroup(factvalue);
269 break;
270 case "owner":
271 file.setUser(factvalue);
272 break;
273 case "mode": {
274 final int off = factvalue.length() - 3; // only parse last 3 digits
275 for (int i = 0; i < 3; i++) {
276 final int ch = factvalue.charAt(off + i) - '0';
277 if (ch >= 0 && ch <= 7) { // Check it's valid octal
278 for (final int p : UNIX_PERMS[ch]) {
279 file.setPermission(UNIX_GROUPS[i], p, true);
280 }
281 } else {
282 // TODO should this cause failure, or can it be reported somehow?
283 }
284 } // digits
285 break;
286 }
287 default:
288 break;
289 } // mode
290 // unix.
291 } else if (!hasUnixMode && "perm".equals(factname)) { // skip if we have the UNIX.mode
292 doUnixPerms(file, valueLowerCase);
293 }
294 break;
295 } // process "perm"
296 } // each fact
297 return file;
298 }
299 }