001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.util; 019 020import java.io.File; 021import java.nio.file.Files; 022import java.nio.file.InvalidPathException; 023import java.nio.file.Path; 024import java.nio.file.Paths; 025import java.util.ArrayList; 026import java.util.Arrays; 027import java.util.List; 028import java.util.logging.Level; 029import java.util.logging.Logger; 030import java.util.regex.Pattern; 031 032import org.syncany.util.EnvironmentUtil.OperatingSystem; 033 034public class NormalizedPath { 035 protected static final Logger logger = Logger.getLogger(NormalizedPath.class.getSimpleName()); 036 private static final Pattern ILLEGAL_CHARS_PATTERN_WINDOWS = Pattern.compile("[\\\\/:*?\"<>|\0]"); 037 private static final Pattern ILLEGAL_CHARS_PATTERN_UNIX_LIKE = Pattern.compile("[\0]"); 038 private static final Pattern ILLEGAL_NON_ASCII_CHARS_PATTERN = Pattern.compile("[^a-zA-Z0-9., ]"); 039 040 protected File root; 041 protected String normalizedPath; 042 043 public NormalizedPath(File root, String normalizedPath) { 044 this.root = root; 045 this.normalizedPath = normalizedPath; 046 } 047 048 @Override 049 public String toString() { 050 return normalizedPath; 051 } 052 053 public NormalizedPath getParent() { 054 int lastIndexOfSlash = normalizedPath.lastIndexOf("/"); 055 056 if (lastIndexOfSlash == -1) { 057 return new NormalizedPath(root, ""); 058 } 059 else { 060 return new NormalizedPath(root, normalizedPath.substring(0, lastIndexOfSlash)); 061 } 062 } 063 064 private List<String> getParts() { 065 return Arrays.asList(normalizedPath.split("[/]")); 066 } 067 068 public File toFile() { 069 if (root != null) { 070 return new File(root, normalizedPath); 071 } 072 else { 073 return new File(normalizedPath); 074 } 075 } 076 077 private boolean canCreate(String pathPart) { 078 try { 079 Path tempFile = Files.createTempFile(pathPart, "canCreate"); 080 Files.deleteIfExists(tempFile); 081 082 return true; 083 } 084 catch (Exception e) { 085 logger.log(Level.SEVERE, "WARNING: Cannot create file: "+pathPart); 086 return false; 087 } 088 } 089 090 public boolean hasIllegalChars() { 091 return hasIllegalChars(normalizedPath); 092 } 093 094 private String getExtension(boolean includeDot) { 095 return getExtension(normalizedPath, includeDot); 096 } 097 098 private String getExtension(String filename, boolean includeDot) { 099 int lastDot = filename.lastIndexOf("."); 100 int lastSlash = filename.lastIndexOf("/"); 101 102 if (lastDot == -1 || lastSlash > lastDot) { 103 return ""; 104 } 105 106 String extension = filename.substring(lastDot + 1, filename.length()); 107 return (includeDot) ? "." + extension : extension; 108 } 109 110 private String getPathWithoutExtension(String filename) { 111 String extension = getExtension(true); // .txt 112 113 if ("".equals(extension)) { 114 return filename; 115 } 116 else { 117 return filename.substring(0, filename.length() - extension.length()); 118 } 119 } 120 121 private boolean hasIllegalChars(String pathPart) { 122 if (EnvironmentUtil.isWindows() && ILLEGAL_CHARS_PATTERN_WINDOWS.matcher(pathPart).find()) { 123 return true; 124 } 125 else if (EnvironmentUtil.isUnixLikeOperatingSystem() && ILLEGAL_CHARS_PATTERN_UNIX_LIKE.matcher(pathPart).find()) { 126 return true; 127 } 128 else { 129 return false; 130 } 131 } 132 133 private String cleanIllegalChars(String pathPart) { 134 if (EnvironmentUtil.isWindows()) { 135 return ILLEGAL_CHARS_PATTERN_WINDOWS.matcher(pathPart).replaceAll(""); 136 } 137 else { 138 return ILLEGAL_CHARS_PATTERN_UNIX_LIKE.matcher(pathPart).replaceAll(""); 139 } 140 } 141 142 private String cleanAsciiOnly(String pathPart) { 143 return ILLEGAL_NON_ASCII_CHARS_PATTERN.matcher(pathPart).replaceAll(""); 144 } 145 146 private String addFilenameConflictSuffix(String pathPart, String filenameSuffix) { 147 String conflictFileExtension = getExtension(pathPart, false); 148 boolean originalFileHasExtension = conflictFileExtension != null && !"".equals(conflictFileExtension); 149 150 if (originalFileHasExtension) { 151 String conflictFileBasename = getPathWithoutExtension(pathPart); 152 return String.format("%s (%s).%s", conflictFileBasename, filenameSuffix, conflictFileExtension); 153 } 154 else { 155 return String.format("%s (%s)", pathPart, filenameSuffix); 156 } 157 } 158 159 public NormalizedPath withSuffix(String filenameSuffix, boolean canExist) throws Exception { 160 if (canExist) { 161 return toCreatable(filenameSuffix, 0); 162 } 163 else { 164 NormalizedPath creatableNormalizedPath = null; 165 int attempt = 0; 166 167 do { 168 String aFilenameSuffix = (attempt > 0) ? filenameSuffix + " " + attempt : filenameSuffix; 169 creatableNormalizedPath = new NormalizedPath(root, addFilenameConflictSuffix(normalizedPath.toString(), aFilenameSuffix)); 170 boolean exists = FileUtil.exists(creatableNormalizedPath.toFile()); 171 172 if (!exists) { 173 return creatableNormalizedPath; 174 } 175 } while (attempt++ < 200); 176 177 throw new Exception("Cannot create path with suffix; "+attempt+" attempts: "+creatableNormalizedPath); 178 } 179 } 180 181 /* pictures/ 182 * some/ 183 * folder/ 184 * file.jpg 185 * some\\folder/ 186 * -> file.jpg 187 * 188 * relativeNormalizedPath = pictures/some\\folder/file.jpg 189 * 190 * -> createable: pictures/somefolder (filename conflict)/file.jpg 191 * 192 * http://msdn.microsoft.com/en-us/library/system.io.path.getinvalidfilenamechars.aspx 193 */ 194 public NormalizedPath toCreatable(String filenameSuffix, boolean canExist) throws Exception { 195 if (canExist) { 196 return toCreatable(filenameSuffix, 0); 197 } 198 else { 199 NormalizedPath creatableNormalizedPath = null; 200 int attempt = 0; 201 202 do { 203 creatableNormalizedPath = toCreatable(filenameSuffix, attempt); 204 boolean exists = FileUtil.exists(creatableNormalizedPath.toFile()); 205 206 // TODO [medium] The exists-check should be in the pathPart-loop, b/c what if fileB is a FILE in this path: folderA/fileB/folderC/file1.jpg 207 208 if (!exists) { 209 return creatableNormalizedPath; 210 } 211 212 logger.log(Level.WARNING, " - File exists, trying new file: " + creatableNormalizedPath.toFile()); 213 } while (attempt++ < 10); 214 215 throw new Exception("Cannot create creatable path; "+creatableNormalizedPath+" attempts: "+attempt); 216 } 217 } 218 219 private NormalizedPath toCreatable(String filenameSuffix, int attempt) { 220 List<String> cleanedRelativePathParts = new ArrayList<String>(); 221 String attemptedFilenameSuffix = (attempt > 0) ? filenameSuffix + " " + attempt : filenameSuffix; 222 223 for (String pathPart : getParts()) { 224 boolean needsCleansing = false; 225 226 // Determine if path part is illegal 227 if (hasIllegalChars(pathPart)) { 228 needsCleansing = true; 229 } 230 else { 231 try { 232 Paths.get(pathPart); 233 } 234 catch (InvalidPathException e) { 235 needsCleansing = true; 236 } 237 } 238 239 // Clean if it is illegal 240 if (needsCleansing) { 241 String cleanedParentPart = addFilenameConflictSuffix(cleanIllegalChars(pathPart), attemptedFilenameSuffix); // TODO [low] attempt does not make sense hree 242 243 // Check if cleaned path actually can be created (creates local file!) 244 if (canCreate(cleanedParentPart)) { 245 pathPart = cleanedParentPart; 246 } 247 else { 248 pathPart = addFilenameConflictSuffix(cleanAsciiOnly(pathPart), attemptedFilenameSuffix); // TODO [low] attempt does not make sense hree 249 } 250 251 logger.log(Level.INFO, " + WAS ILLEGAL: Now: "+pathPart); 252 } 253 254 // Add to path part list 255 cleanedRelativePathParts.add(pathPart); 256 } 257 258 String cleanedRelativeTargetPath = StringUtil.join(cleanedRelativePathParts, File.separator); 259 return new NormalizedPath(root, cleanedRelativeTargetPath); 260 } 261}