MLSxEntryParser.java

  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. package org.apache.commons.net.ftp.parser;

  18. import java.text.ParsePosition;
  19. import java.text.SimpleDateFormat;
  20. import java.time.Instant;
  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. import org.apache.commons.net.ftp.FTPFile;
  28. import org.apache.commons.net.ftp.FTPFileEntryParserImpl;

  29. /**
  30.  * Parser class for MSLT and MLSD replies. See RFC 3659.
  31.  * <p>
  32.  * Format is as follows:
  33.  * </p>
  34.  *
  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.  * <p>
  51.  * A single control response entry (MLST) is returned with a leading space; multiple (data) entries are returned without any leading spaces. The parser requires
  52.  * that the leading space from the MLST entry is removed. MLSD entries can begin with a single space if there are no facts.
  53.  * </p>
  54.  *
  55.  * @since 3.0
  56.  */
  57. public class MLSxEntryParser extends FTPFileEntryParserImpl {
  58.     // This class is immutable, so a single instance can be shared.
  59.     private static final MLSxEntryParser INSTANCE = new MLSxEntryParser();

  60.     private static final HashMap<String, Integer> TYPE_TO_INT = new HashMap<>();
  61.     static {
  62.         TYPE_TO_INT.put("file", Integer.valueOf(FTPFile.FILE_TYPE));
  63.         TYPE_TO_INT.put("cdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // listed directory
  64.         TYPE_TO_INT.put("pdir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // a parent dir
  65.         TYPE_TO_INT.put("dir", Integer.valueOf(FTPFile.DIRECTORY_TYPE)); // dir or sub-dir
  66.     }

  67.     private static final int[] UNIX_GROUPS = { // Groups in order of mode digits
  68.             FTPFile.USER_ACCESS, FTPFile.GROUP_ACCESS, FTPFile.WORLD_ACCESS, };

  69.     private static final int[][] UNIX_PERMS = { // perm bits, broken down by octal int value
  70.             /* 0 */ {}, /* 1 */ { FTPFile.EXECUTE_PERMISSION }, /* 2 */ { FTPFile.WRITE_PERMISSION },
  71.             /* 3 */ { FTPFile.EXECUTE_PERMISSION, FTPFile.WRITE_PERMISSION }, /* 4 */ { FTPFile.READ_PERMISSION },
  72.             /* 5 */ { FTPFile.READ_PERMISSION, FTPFile.EXECUTE_PERMISSION }, /* 6 */ { FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION },
  73.             /* 7 */ { FTPFile.READ_PERMISSION, FTPFile.WRITE_PERMISSION, FTPFile.EXECUTE_PERMISSION }, };

  74.     public static MLSxEntryParser getInstance() {
  75.         return INSTANCE;
  76.     }

  77.     public static FTPFile parseEntry(final String entry) {
  78.         return INSTANCE.parseFTPEntry(entry);
  79.     }

  80.     /**
  81.      * Parse a GMT time stamp of the form yyyyMMDDHHMMSS[.sss]
  82.      *
  83.      * @param timestamp the date-time to parse
  84.      * @return a Calendar entry, may be {@code null}
  85.      * @since 3.4
  86.      */
  87.     public static Calendar parseGMTdateTime(final String timestamp) {
  88.         final SimpleDateFormat dateFormat;
  89.         final boolean hasMillis;
  90.         if (timestamp.contains(".")) {
  91.             dateFormat = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
  92.             hasMillis = true;
  93.         } else {
  94.             dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
  95.             hasMillis = false;
  96.         }
  97.         final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
  98.         // both time zones need to be set for the parse to work OK
  99.         dateFormat.setTimeZone(gmtTimeZone);
  100.         final GregorianCalendar gCalendar = new GregorianCalendar(gmtTimeZone);
  101.         final ParsePosition pos = new ParsePosition(0);
  102.         dateFormat.setLenient(false); // We want to parse the whole string
  103.         final Date parsed = dateFormat.parse(timestamp, pos);
  104.         if (pos.getIndex() != timestamp.length()) {
  105.             return null; // did not fully parse the input
  106.         }
  107.         gCalendar.setTime(parsed);
  108.         if (!hasMillis) {
  109.             gCalendar.clear(Calendar.MILLISECOND); // flag up missing ms units
  110.         }
  111.         return gCalendar;
  112.     }

  113.     /**
  114.      * Parse a GMT time stamp of the form yyyyMMDDHHMMSS[.sss]
  115.      *
  116.      * @param timestamp the date-time to parse
  117.      * @return a Calendar entry, may be {@code null}
  118.      * @since 3.9.0
  119.      */
  120.     public static Instant parseGmtInstant(final String timestamp) {
  121.         return parseGMTdateTime(timestamp).toInstant();
  122.     }

  123.     /**
  124.      * Create the parser for MSLT and MSLD listing entries This class is immutable, so one can use {@link #getInstance()} instead.
  125.      */
  126.     public MLSxEntryParser() {
  127.     }

  128.     // perm-fact = "Perm" "=" *pvals
  129.     // pvals = "a" / "c" / "d" / "e" / "f" /
  130.     // "l" / "m" / "p" / "r" / "w"
  131.     private void doUnixPerms(final FTPFile file, final String valueLowerCase) {
  132.         for (final char c : valueLowerCase.toCharArray()) {
  133.             // TODO these are mostly just guesses at present
  134.             switch (c) {
  135.             case 'a': // (file) may APPEnd
  136.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
  137.                 break;
  138.             case 'c': // (dir) files may be created in the dir
  139.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
  140.                 break;
  141.             case 'd': // deletable
  142.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
  143.                 break;
  144.             case 'e': // (dir) can change to this dir
  145.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
  146.                 break;
  147.             case 'f': // (file) renamable
  148.                 // ?? file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
  149.                 break;
  150.             case 'l': // (dir) can be listed
  151.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.EXECUTE_PERMISSION, true);
  152.                 break;
  153.             case 'm': // (dir) can create directory here
  154.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
  155.                 break;
  156.             case 'p': // (dir) entries may be deleted
  157.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
  158.                 break;
  159.             case 'r': // (files) file may be RETRieved
  160.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.READ_PERMISSION, true);
  161.                 break;
  162.             case 'w': // (files) file may be STORed
  163.                 file.setPermission(FTPFile.USER_ACCESS, FTPFile.WRITE_PERMISSION, true);
  164.                 break;
  165.             default:
  166.                 break;
  167.             // ignore unexpected flag for now.
  168.             } // switch
  169.         } // each char
  170.     }

  171.     @Override
  172.     public FTPFile parseFTPEntry(final String entry) {
  173.         if (entry.startsWith(" ")) { // leading space means no facts are present
  174.             if (entry.length() > 1) { // is there a path name?
  175.                 final FTPFile file = new FTPFile();
  176.                 file.setRawListing(entry);
  177.                 file.setName(entry.substring(1));
  178.                 return file;
  179.             }
  180.             return null; // Invalid - no pathname

  181.         }
  182.         final String[] parts = entry.split(" ", 2); // Path may contain space
  183.         if (parts.length != 2 || parts[1].isEmpty()) {
  184.             return null; // no space found or no file name
  185.         }
  186.         final String factList = parts[0];
  187.         if (!factList.endsWith(";")) {
  188.             return null;
  189.         }
  190.         final FTPFile file = new FTPFile();
  191.         file.setRawListing(entry);
  192.         file.setName(parts[1]);
  193.         final String[] facts = factList.split(";");
  194.         final boolean hasUnixMode = parts[0].toLowerCase(Locale.ENGLISH).contains("unix.mode=");
  195.         for (final String fact : facts) {
  196.             final String[] factparts = fact.split("=", -1); // Don't drop empty values
  197. // Sample missing permission
  198. // drwx------   2 mirror   mirror       4096 Mar 13  2010 subversion
  199. // modify=20100313224553;perm=;type=dir;unique=811U282598;UNIX.group=500;UNIX.mode=0700;UNIX.owner=500; subversion
  200.             if (factparts.length != 2) {
  201.                 return null; // invalid - there was no "=" sign
  202.             }
  203.             final String factname = factparts[0].toLowerCase(Locale.ENGLISH);
  204.             final String factvalue = factparts[1];
  205.             if (factvalue.isEmpty()) {
  206.                 continue; // nothing to see here
  207.             }
  208.             final String valueLowerCase = factvalue.toLowerCase(Locale.ENGLISH);
  209.             if ("size".equals(factname) || "sizd".equals(factname)) {
  210.                 file.setSize(Long.parseLong(factvalue));
  211.             } else if ("modify".equals(factname)) {
  212.                 final Calendar parsed = parseGMTdateTime(factvalue);
  213.                 if (parsed == null) {
  214.                     return null;
  215.                 }
  216.                 file.setTimestamp(parsed);
  217.             } else if ("type".equals(factname)) {
  218.                 final Integer intType = TYPE_TO_INT.get(valueLowerCase);
  219.                 if (intType == null) {
  220.                     file.setType(FTPFile.UNKNOWN_TYPE);
  221.                 } else {
  222.                     file.setType(intType.intValue());
  223.                 }
  224.             } else if (factname.startsWith("unix.")) {
  225.                 final String unixfact = factname.substring("unix.".length()).toLowerCase(Locale.ENGLISH);
  226.                 if ("group".equals(unixfact)) {
  227.                     file.setGroup(factvalue);
  228.                 } else if ("owner".equals(unixfact)) {
  229.                     file.setUser(factvalue);
  230.                 } else if ("mode".equals(unixfact)) { // e.g. 0[1]755
  231.                     final int off = factvalue.length() - 3; // only parse last 3 digits
  232.                     for (int i = 0; i < 3; i++) {
  233.                         final int ch = factvalue.charAt(off + i) - '0';
  234.                         if (ch >= 0 && ch <= 7) { // Check it's valid octal
  235.                             for (final int p : UNIX_PERMS[ch]) {
  236.                                 file.setPermission(UNIX_GROUPS[i], p, true);
  237.                             }
  238.                         } else {
  239.                             // TODO should this cause failure, or can it be reported somehow?
  240.                         }
  241.                     } // digits
  242.                 } // mode
  243.             } // unix.
  244.             else if (!hasUnixMode && "perm".equals(factname)) { // skip if we have the UNIX.mode
  245.                 doUnixPerms(file, valueLowerCase);
  246.             } // process "perm"
  247.         } // each fact
  248.         return file;
  249.     }
  250. }