aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/vfs/WindowsOsPathPolicy.java
blob: f420683b75afbcbd11fbd258bc0afd0e1bc999f5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// 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;
  }
}