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