/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gradle.api.internal.file;
import org.apache.commons.lang.StringUtils;
import org.gradle.api.UncheckedIOException;
import org.gradle.internal.nativeintegration.filesystem.FileSystem;
import org.gradle.internal.os.OperatingSystem;
import org.gradle.util.CollectionUtils;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
class FileNormaliser {
private static final String FILE_PATH_SEPARATORS = File.separatorChar != '/' ? ("/" + File.separator) : File.separator;
private final FileSystem fileSystem;
private final boolean isWindowsOs;
FileNormaliser(FileSystem fileSystem) {
this(fileSystem, OperatingSystem.current());
}
FileNormaliser(FileSystem fileSystem, OperatingSystem operatingSystem) {
this.fileSystem = fileSystem;
this.isWindowsOs = operatingSystem.isWindows();
}
// normalizes a path in similar ways as File.getCanonicalFile(), except that it
// does NOT resolve symlinks (by design)
public File normalise(File file) {
try {
if (!file.isAbsolute()) {
throw new IllegalArgumentException(String.format("Cannot normalize a relative file: '%s'", file));
}
if (isWindowsOs) {
// on Windows, File.getCanonicalFile() doesn't resolve symlinks
return file.getCanonicalFile();
}
File candidate;
String filePath = file.getPath();
List<String> path = null;
if (isNormalisingRequiredForAbsolutePath(filePath)) {
path = splitAndNormalisePath(filePath);
String resolvedPath = CollectionUtils.join(File.separator, path);
boolean needLeadingSeparator = File.listRoots()[0].getPath().startsWith(File.separator);
if (needLeadingSeparator) {
resolvedPath = File.separator + resolvedPath;
}
candidate = new File(resolvedPath);
} else {
candidate = file;
}
// If the file system is case sensitive, we don't have to normalise it further
if (fileSystem.isCaseSensitive()) {
return candidate;
}
// Short-circuit the slower lookup method by using the canonical file
File canonical = candidate.getCanonicalFile();
if (candidate.getPath().equalsIgnoreCase(canonical.getPath())) {
return canonical;
}
// Canonical path is different to what we expected (eg there is a link somewhere in there). Normalise a segment at a time
// TODO - start resolving only from where the expected and canonical paths are different
if (path == null) {
path = splitAndNormalisePath(filePath);
}
return normaliseUnixPathIgnoringCase(path);
} catch (IOException e) {
throw new UncheckedIOException(String.format("Could not normalize path for file '%s'.", file), e);
}
}
boolean isNormalisingRequiredForAbsolutePath(String filePath) {
if (File.pathSeparatorChar != '/') {
filePath = filePath.replace(File.pathSeparatorChar, '/');
}
if (filePath.charAt(0) != '/') {
throw new IllegalArgumentException("Only absolute unix paths are currently handled. filePath=" + filePath);
}
if (filePath.contains("/../") || filePath.contains("/./") || filePath.contains("//")) {
return true;
}
if (filePath.endsWith("/") || filePath.endsWith("/.") || filePath.endsWith("/..")) {
return true;
}
return false;
}
private List<String> splitAndNormalisePath(String filePath) {
String[] segments = splitPath(filePath);
List<String> path = new ArrayList<String>(segments.length);
for (String segment : segments) {
if (segment.equals("..")) {
if (!path.isEmpty()) {
path.remove(path.size() - 1);
}
} else if (!segment.equals(".") && segment.length() > 0) {
path.add(segment);
}
}
return path;
}
private String[] splitPath(String filePath) {
return StringUtils.split(filePath, FILE_PATH_SEPARATORS);
}
private File normaliseUnixPathIgnoringCase(List<String> path) throws IOException {
File current = File.listRoots()[0];
for (int pos = 0; pos < path.size(); pos++) {
File child = findChildIgnoringCase(current, path.get(pos));
if (child == null) {
current = new File(current, CollectionUtils.join(File.separator, path.subList(pos, path.size())));
break;
}
current = child;
}
return current;
}
private File findChildIgnoringCase(File current, String segment) throws IOException {
String[] children = current.list();
if (children == null) {
return null;
}
// TODO - find some native methods for doing this
for (String child : children) {
if (child.equalsIgnoreCase(segment)) {
return new File(current, child);
}
}
return new File(current, segment);
}
}