// Copyright 2017 The Bazel Authors. All rights reserved. // // 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 com.google.devtools.build.lib.vfs; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.windows.WindowsShortPath; import com.google.devtools.build.lib.windows.jni.WindowsFileOperations; import java.io.IOException; @VisibleForTesting class WindowsOsPathPolicy implements OsPathPolicy { static final WindowsOsPathPolicy INSTANCE = new WindowsOsPathPolicy(); static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1; private static final Splitter WINDOWS_PATH_SPLITTER = Splitter.onPattern("[\\\\/]+").omitEmptyStrings(); private final ShortPathResolver shortPathResolver; interface ShortPathResolver { String resolveShortPath(String path); } static class DefaultShortPathResolver implements ShortPathResolver { @Override public String resolveShortPath(String path) { try { return WindowsFileOperations.getLongPath(path); } catch (IOException e) { return path; } } } WindowsOsPathPolicy() { this(new DefaultShortPathResolver()); } WindowsOsPathPolicy(ShortPathResolver shortPathResolver) { this.shortPathResolver = shortPathResolver; } @Override public int needsToNormalize(String path) { int n = path.length(); int normalizationLevel = NORMALIZED; int dotCount = 0; char prevChar = 0; int segmentBeginIndex = 0; // The start index of the current path index boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test for (int i = 0; i < n; i++) { char c = path.charAt(i); if (isSeparator(c)) { if (c == '\\') { normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); } // No need to check for '\\' here because that already causes normalization if (prevChar == '/') { normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); } if (dotCount == 1 || dotCount == 2) { normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); } if (segmentHasShortPathChar) { if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) { normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION); } } segmentBeginIndex = i + 1; segmentHasShortPathChar = false; } else if (c == '~') { // This path segment might be a Windows short path segment segmentHasShortPathChar = true; } dotCount = c == '.' ? dotCount + 1 : 0; prevChar = c; } if (segmentHasShortPathChar) { if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex))) { normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION); } } if ((n > 1 && isSeparator(prevChar)) || dotCount == 1 || dotCount == 2) { normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE); } return normalizationLevel; } @Override public int needsToNormalizeSuffix(String normalizedSuffix) { // On Windows, all bets are off because of short paths, so we have to check the entire string return needsToNormalize(normalizedSuffix); } @Override public String normalize(String path, int normalizationLevel) { if (normalizationLevel == NORMALIZED) { return path; } if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) { String resolvedPath = shortPathResolver.resolveShortPath(path); if (resolvedPath != null) { path = resolvedPath; } } String[] segments = Iterables.toArray(WINDOWS_PATH_SPLITTER.splitToList(path), String.class); int driveStrLength = getDriveStrLength(path); boolean isAbsolute = driveStrLength > 0; int segmentSkipCount = isAbsolute && driveStrLength > 1 ? 1 : 0; StringBuilder sb = new StringBuilder(path.length()); if (isAbsolute) { char c = path.charAt(0); if (isSeparator(c)) { sb.append('/'); } else { sb.append(Character.toUpperCase(c)); sb.append(":/"); } } int segmentCount = Utils.removeRelativePaths(segments, segmentSkipCount, isAbsolute); for (int i = 0; i < segmentCount; ++i) { sb.append(segments[i]); sb.append('/'); } if (segmentCount > 0) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } @Override public int getDriveStrLength(String path) { int n = path.length(); if (n == 0) { return 0; } char c0 = path.charAt(0); if (isSeparator(c0)) { return 1; } if (n < 3) { return 0; } char c1 = path.charAt(1); char c2 = path.charAt(2); if (isDriveLetter(c0) && c1 == ':' && isSeparator(c2)) { return 3; } return 0; } private static boolean isDriveLetter(char c) { return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')); } @Override public int compare(String s1, String s2) { // Windows is case-insensitive return s1.compareToIgnoreCase(s2); } @Override public int compare(char c1, char c2) { return Character.compare(Character.toLowerCase(c1), Character.toLowerCase(c2)); } @Override public boolean equals(String s1, String s2) { return s1.equalsIgnoreCase(s2); } @Override public int hash(String s) { // Windows is case-insensitive return s.toLowerCase().hashCode(); } @Override public boolean startsWith(String path, String prefix) { int pathn = path.length(); int prefixn = prefix.length(); if (pathn < prefixn) { return false; } for (int i = 0; i < prefixn; ++i) { if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) { return false; } } return true; } @Override public boolean endsWith(String path, String suffix) { int pathn = path.length(); int suffixLength = suffix.length(); if (pathn < suffixLength) { return false; } int offset = pathn - suffixLength; for (int i = 0; i < suffixLength; ++i) { if (Character.toLowerCase(path.charAt(i + offset)) != Character.toLowerCase(suffix.charAt(i))) { return false; } } return true; } @Override public char getSeparator() { return '/'; } @Override public boolean isSeparator(char c) { return c == '/' || c == '\\'; } @Override public boolean isCaseSensitive() { return false; } }