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