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