1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.shared.filtering;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.OutputStream;
24 import java.io.Reader;
25 import java.io.Writer;
26 import java.nio.charset.Charset;
27 import java.nio.file.Files;
28 import java.nio.file.NoSuchFileException;
29 import java.nio.file.attribute.PosixFilePermission;
30 import java.util.EnumSet;
31 import java.util.StringTokenizer;
32 import java.util.regex.Pattern;
33
34 import org.apache.commons.lang3.SystemUtils;
35 import org.codehaus.plexus.util.io.CachingOutputStream;
36 import org.codehaus.plexus.util.io.CachingWriter;
37
38 /**
39 * @author Olivier Lamy
40 * @author Dennis Lundberg
41 */
42 public final class FilteringUtils {
43 /**
44 * The number of bytes in a kilobyte.
45 */
46 private static final int ONE_KB = 1024;
47
48 /**
49 * The number of bytes in a megabyte.
50 */
51 private static final int ONE_MB = ONE_KB * ONE_KB;
52
53 /**
54 * The file copy buffer size (30 MB)
55 */
56 private static final int FILE_COPY_BUFFER_SIZE = ONE_MB * 30;
57
58 private static final String WINDOWS_PATH_PATTERN = "^(.*)[a-zA-Z]:\\\\(.*)";
59
60 private static final Pattern PATTERN = Pattern.compile(WINDOWS_PATH_PATTERN);
61 public static final int COPY_BUFFER_LENGTH = 8192;
62
63 /**
64 *
65 */
66 private FilteringUtils() {
67 // nothing just an util class
68 }
69
70 // TODO: Correct to handle relative windows paths. (http://jira.apache.org/jira/browse/MSHARED-121)
71 // How do we distinguish a relative windows path from some other value that happens to contain backslashes??
72 /**
73 * @param val The value to be escaped.
74 * @return Escaped value
75 */
76 public static String escapeWindowsPath(String val) {
77 if (!isEmpty(val) && PATTERN.matcher(val).matches()) {
78 // Adapted from StringUtils.replace in plexus-utils to accommodate pre-escaped backslashes.
79 StringBuilder buf = new StringBuilder(val.length());
80 int start = 0, end = 0;
81 while ((end = val.indexOf('\\', start)) != -1) {
82 buf.append(val, start, end).append("\\\\");
83 start = end + 1;
84
85 if (val.indexOf('\\', end + 1) == end + 1) {
86 start++;
87 }
88 }
89
90 buf.append(val.substring(start));
91
92 return buf.toString();
93 }
94 return val;
95 }
96
97 /**
98 * Resolve a file <code>filename</code> to its canonical form. If <code>filename</code> is
99 * relative (doesn't start with <code>/</code>), it is resolved relative to
100 * <code>baseFile</code>. Otherwise it is treated as a normal root-relative path.
101 *
102 * @param baseFile where to resolve <code>filename</code> from, if <code>filename</code> is relative
103 * @param filename absolute or relative file path to resolve
104 * @return the canonical <code>File</code> of <code>filename</code>
105 */
106 public static File resolveFile(final File baseFile, String filename) {
107 String filenm = filename;
108 if ('/' != File.separatorChar) {
109 filenm = filename.replace('/', File.separatorChar);
110 }
111
112 if ('\\' != File.separatorChar) {
113 filenm = filename.replace('\\', File.separatorChar);
114 }
115
116 // deal with absolute files
117 if (filenm.startsWith(File.separator) || (SystemUtils.IS_OS_WINDOWS && filenm.indexOf(":") > 0)) {
118 File file = new File(filenm);
119
120 try {
121 file = file.getCanonicalFile();
122 } catch (final IOException ioe) {
123 // nop
124 }
125
126 return file;
127 }
128 // FIXME: I'm almost certain this // removal is unnecessary, as getAbsoluteFile() strips
129 // them. However, I'm not sure about this UNC stuff. (JT)
130 final char[] chars = filename.toCharArray();
131 final StringBuilder sb = new StringBuilder();
132
133 // remove duplicate file separators in succession - except
134 // on win32 at start of filename as UNC filenames can
135 // be \\AComputer\AShare\myfile.txt
136 int start = 0;
137 if ('\\' == File.separatorChar) {
138 sb.append(filenm.charAt(0));
139 start++;
140 }
141
142 for (int i = start; i < chars.length; i++) {
143 final boolean doubleSeparator = File.separatorChar == chars[i] && File.separatorChar == chars[i - 1];
144
145 if (!doubleSeparator) {
146 sb.append(chars[i]);
147 }
148 }
149
150 filenm = sb.toString();
151
152 // must be relative
153 File file = (new File(baseFile, filenm)).getAbsoluteFile();
154
155 try {
156 file = file.getCanonicalFile();
157 } catch (final IOException ioe) {
158 // nop
159 }
160
161 return file;
162 }
163
164 /**
165 * <p>This method can calculate the relative path between two paths on a file system.</p>
166 * <pre>
167 * PathTool.getRelativeFilePath( null, null ) = ""
168 * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" ) = ""
169 * PathTool.getRelativeFilePath( "/usr/local", null ) = ""
170 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" ) = "java/bin"
171 * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" ) = "java/bin"
172 * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" ) = "../.."
173 * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh"
174 * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.."
175 * PathTool.getRelativeFilePath( "/usr/local/", "/bin" ) = "../../bin"
176 * PathTool.getRelativeFilePath( "/bin", "/usr/local/" ) = "../usr/local"
177 * </pre>
178 * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character.
179 *
180 * @param oldPath old path
181 * @param newPath new path
182 * @return a relative file path from <code>oldPath</code>.
183 */
184 public static String getRelativeFilePath(final String oldPath, final String newPath) {
185 if (isEmpty(oldPath) || isEmpty(newPath)) {
186 return "";
187 }
188
189 // normalise the path delimiters
190 String fromPath = new File(oldPath).getPath();
191 String toPath = new File(newPath).getPath();
192
193 // strip any leading slashes if its a windows path
194 if (toPath.matches("^\\[a-zA-Z]:")) {
195 toPath = toPath.substring(1);
196 }
197 if (fromPath.matches("^\\[a-zA-Z]:")) {
198 fromPath = fromPath.substring(1);
199 }
200
201 // lowercase windows drive letters.
202 if (fromPath.startsWith(":", 1)) {
203 fromPath = Character.toLowerCase(fromPath.charAt(0)) + fromPath.substring(1);
204 }
205 if (toPath.startsWith(":", 1)) {
206 toPath = Character.toLowerCase(toPath.charAt(0)) + toPath.substring(1);
207 }
208
209 // check for the presence of windows drives. No relative way of
210 // traversing from one to the other.
211 if ((toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))
212 && (!toPath.substring(0, 1).equals(fromPath.substring(0, 1)))) {
213 // they both have drive path element but they dont match, no
214 // relative path
215 return null;
216 }
217
218 if ((toPath.startsWith(":", 1) && !fromPath.startsWith(":", 1))
219 || (!toPath.startsWith(":", 1) && fromPath.startsWith(":", 1))) {
220 // one has a drive path element and the other doesnt, no relative
221 // path.
222 return null;
223 }
224
225 String resultPath = buildRelativePath(toPath, fromPath, File.separatorChar);
226
227 if (newPath.endsWith(File.separator) && !resultPath.endsWith(File.separator)) {
228 return resultPath + File.separator;
229 }
230
231 return resultPath;
232 }
233
234 private static String buildRelativePath(String toPath, String fromPath, final char separatorChar) {
235 // use tokeniser to traverse paths and for lazy checking
236 StringTokenizer toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar));
237 StringTokenizer fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar));
238
239 int count = 0;
240
241 // walk along the to path looking for divergence from the from path
242 while (toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens()) {
243 if (separatorChar == '\\') {
244 if (!fromTokeniser.nextToken().equalsIgnoreCase(toTokeniser.nextToken())) {
245 break;
246 }
247 } else {
248 if (!fromTokeniser.nextToken().equals(toTokeniser.nextToken())) {
249 break;
250 }
251 }
252
253 count++;
254 }
255
256 // reinitialise the tokenisers to count positions to retrieve the
257 // gobbled token
258
259 toTokeniser = new StringTokenizer(toPath, String.valueOf(separatorChar));
260 fromTokeniser = new StringTokenizer(fromPath, String.valueOf(separatorChar));
261
262 while (count-- > 0) {
263 fromTokeniser.nextToken();
264 toTokeniser.nextToken();
265 }
266
267 StringBuilder relativePath = new StringBuilder();
268
269 // add back refs for the rest of from location.
270 while (fromTokeniser.hasMoreTokens()) {
271 fromTokeniser.nextToken();
272
273 relativePath.append("..");
274
275 if (fromTokeniser.hasMoreTokens()) {
276 relativePath.append(separatorChar);
277 }
278 }
279
280 if (relativePath.length() != 0 && toTokeniser.hasMoreTokens()) {
281 relativePath.append(separatorChar);
282 }
283
284 // add fwd fills for whatevers left of newPath.
285 while (toTokeniser.hasMoreTokens()) {
286 relativePath.append(toTokeniser.nextToken());
287
288 if (toTokeniser.hasMoreTokens()) {
289 relativePath.append(separatorChar);
290 }
291 }
292 return relativePath.toString();
293 }
294
295 static boolean isEmpty(final String string) {
296 return string == null || string.trim().isEmpty();
297 }
298
299 /**
300 * <b>If wrappers is null or empty, the file will be copy only if to.lastModified() < from.lastModified() or if
301 * overwrite is true</b>.
302 *
303 * @param from the file to copy
304 * @param to the destination file
305 * @param encoding the file output encoding (only if wrappers is not empty)
306 * @param wrappers array of {@link FilterWrapper}
307 * @throws IOException if an IO error occurs during copying or filtering
308 */
309 public static void copyFile(File from, File to, String encoding, FilterWrapper[] wrappers) throws IOException {
310 setReadWritePermissions(to);
311
312 if (wrappers == null || wrappers.length == 0) {
313 try (OutputStream os = new CachingOutputStream(to.toPath())) {
314 Files.copy(from.toPath(), os);
315 }
316 } else {
317 Charset charset = charset(encoding);
318 try (Reader fileReader = Files.newBufferedReader(from.toPath(), charset)) {
319 Reader wrapped = fileReader;
320 for (FilterWrapper wrapper : wrappers) {
321 wrapped = wrapper.getReader(wrapped);
322 }
323 try (Writer writer = new CachingWriter(to.toPath(), charset)) {
324 char[] buffer = new char[COPY_BUFFER_LENGTH];
325 int nRead;
326 while ((nRead = wrapped.read(buffer, 0, COPY_BUFFER_LENGTH)) >= 0) {
327 writer.write(buffer, 0, nRead);
328 }
329 }
330 }
331 }
332
333 copyFilePermissions(from, to);
334 }
335
336 /**
337 * <b>If wrappers is null or empty, the file will be copy only if to.lastModified() < from.lastModified() or if
338 * overwrite is true</b>.
339 *
340 * @param from the file to copy
341 * @param to the destination file
342 * @param encoding the file output encoding (only if wrappers is not empty)
343 * @param wrappers array of {@link FilterWrapper}
344 * @param overwrite unused
345 * @throws IOException if an IO error occurs during copying or filtering
346 * @deprecated use {@link #copyFile(File, File, String, FilterWrapper[])} instead
347 */
348 @Deprecated
349 public static void copyFile(File from, File to, String encoding, FilterWrapper[] wrappers, boolean overwrite)
350 throws IOException {
351 copyFile(from, to, encoding, wrappers);
352 }
353
354 /**
355 * Attempts to copy file permissions from the source to the destination file.
356 * Initially attempts to copy posix file permissions, assuming that the files are both on posix filesystems.
357 * If the initial attempts fail then a second attempt using less precise permissions model.
358 * Note that permissions are copied on a best-efforts basis,
359 * failure to copy permissions will not result in an exception.
360 *
361 * @param source the file to copy permissions from.
362 * @param destination the file to copy permissions to.
363 */
364 private static void copyFilePermissions(File source, File destination) throws IOException {
365 try {
366 // attempt to copy posix file permissions
367 Files.setPosixFilePermissions(destination.toPath(), Files.getPosixFilePermissions(source.toPath()));
368 } catch (NoSuchFileException nsfe) {
369 // ignore if destination file or symlink does not exist
370 } catch (UnsupportedOperationException e) {
371 // fallback to setting partial permissions
372 destination.setExecutable(source.canExecute());
373 destination.setReadable(source.canRead());
374 destination.setWritable(source.canWrite());
375 }
376 }
377
378 @SuppressWarnings("ResultOfMethodCallIgnored")
379 private static void setReadWritePermissions(File file) throws IOException {
380 if (file.exists()) {
381 try {
382 Files.setPosixFilePermissions(
383 file.toPath(),
384 EnumSet.of(
385 PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
386 PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
387 PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE));
388 } catch (UnsupportedOperationException e) {
389 file.setReadable(true);
390 file.setWritable(true);
391 }
392 }
393 }
394
395 private static Charset charset(String encoding) {
396 if (encoding == null || encoding.isEmpty()) {
397 return Charset.defaultCharset();
398 } else {
399 return Charset.forName(encoding);
400 }
401 }
402 }