blob: a32a4e356cae5b2a11ea7e430fb4d5b0be40cd3f [file] [log] [blame]
tomlu4a2f2c52017-12-12 12:32:22 -08001// Copyright 2017 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.lib.vfs;
15
16import com.google.common.annotations.VisibleForTesting;
17import com.google.common.base.Preconditions;
18import com.google.devtools.build.lib.util.OS;
19import com.google.devtools.build.lib.windows.WindowsShortPath;
20import com.google.devtools.build.lib.windows.jni.WindowsFileOperations;
21import java.io.IOException;
aehligc801c392017-12-19 07:12:25 -080022import java.util.Arrays;
23import java.util.concurrent.atomic.AtomicReference;
tomlu4a2f2c52017-12-12 12:32:22 -080024import javax.annotation.Nullable;
25
26/**
27 * A local file path representing a file on the host machine. You should use this when you want to
28 * access local files via the file system.
29 *
30 * <p>Paths are either absolute or relative.
31 *
32 * <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
33 * slashes ('/') removed, and any trailing slash also removed. The current implementation does not
34 * touch the incoming path string unless the string actually needs to be normalized.
35 *
36 * <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in
37 * front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as
38 * are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like
39 * \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward
40 * slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during
41 * normalization.
42 *
43 * <p>Mac and Windows file paths are case insensitive. Case is preserved.
44 *
45 * <p>This class is replaces {@link Path} as the way to access the host machine's file system.
46 * Developers should use this class instead of {@link Path}.
47 */
48public final class LocalPath implements Comparable<LocalPath> {
49 private static final OsPathPolicy DEFAULT_OS = createFilePathOs();
50
51 public static final LocalPath EMPTY = create("");
52
53 private final String path;
54 private final int driveStrLength; // 0 for relative paths, 1 on Unix, 3 on Windows
55 private final OsPathPolicy os;
56
57 /** Creates a local path that is specific to the host OS. */
58 public static LocalPath create(String path) {
59 return createWithOs(path, DEFAULT_OS);
60 }
61
62 @VisibleForTesting
63 static LocalPath createWithOs(String path, OsPathPolicy os) {
64 Preconditions.checkNotNull(path);
65 int normalizationLevel = os.needsToNormalize(path);
66 String normalizedPath = os.normalize(path, normalizationLevel);
67 int driveStrLength = os.getDriveStrLength(normalizedPath);
68 return new LocalPath(normalizedPath, driveStrLength, os);
69 }
70
71 /** This method expects path to already be normalized. */
72 private LocalPath(String path, int driveStrLength, OsPathPolicy os) {
73 this.path = Preconditions.checkNotNull(path);
74 this.driveStrLength = driveStrLength;
75 this.os = Preconditions.checkNotNull(os);
76 }
77
78 public String getPathString() {
79 return path;
80 }
81
82 /**
83 * If called on a {@link LocalPath} instance for a mount name (eg. '/' or 'C:/'), the empty string
84 * is returned.
85 */
86 public String getBaseName() {
87 int lastSeparator = path.lastIndexOf(os.getSeparator());
88 return lastSeparator < driveStrLength
89 ? path.substring(driveStrLength)
90 : path.substring(lastSeparator + 1);
91 }
92
93 /**
94 * Returns a {@link LocalPath} instance representing the relative path between this {@link
95 * LocalPath} and the given {@link LocalPath}.
96 *
97 * <pre>
98 * Example:
99 *
100 * LocalPath.create("/foo").getRelative(LocalPath.create("bar/baz"))
101 * -> "/foo/bar/baz"
102 * </pre>
103 *
104 * <p>If the passed path is absolute it is returned untouched. This can be useful to resolve
105 * symlinks.
106 */
107 public LocalPath getRelative(LocalPath other) {
108 Preconditions.checkNotNull(other);
109 Preconditions.checkArgument(os == other.os);
110 return getRelative(other.getPathString(), other.driveStrLength);
111 }
112
113 /**
114 * Returns a {@link LocalPath} instance representing the relative path between this {@link
115 * LocalPath} and the given path.
116 *
117 * <p>See {@link #getRelative(LocalPath)} for details.
118 */
119 public LocalPath getRelative(String other) {
120 Preconditions.checkNotNull(other);
121 return getRelative(other, os.getDriveStrLength(other));
122 }
123
124 private LocalPath getRelative(String other, int otherDriveStrLength) {
125 if (path.isEmpty()) {
126 return create(other);
127 }
128 if (other.isEmpty()) {
129 return this;
130 }
131 // Note that even if other came from a LocalPath instance we still might
132 // need to normalize the result if (for instance) other is a path that
133 // starts with '..'
134 int normalizationLevel = os.needsToNormalize(other);
135 // This is an absolute path, simply return it
136 if (otherDriveStrLength > 0) {
137 String normalizedPath = os.normalize(other, normalizationLevel);
138 return new LocalPath(normalizedPath, otherDriveStrLength, os);
139 }
140 String newPath;
141 if (path.length() == driveStrLength) {
142 newPath = path + other;
143 } else {
144 newPath = path + '/' + other;
145 }
146 newPath = os.normalize(newPath, normalizationLevel);
147 return new LocalPath(newPath, driveStrLength, os);
148 }
149
150 /**
151 * Returns the parent directory of this {@link LocalPath}.
152 *
153 * <p>If this is called on an single directory for a relative path, this returns an empty relative
154 * path. If it's called on a root (like '/') or the empty string, it returns null.
155 */
156 @Nullable
157 public LocalPath getParentDirectory() {
158 int lastSeparator = path.lastIndexOf(os.getSeparator());
159
160 // For absolute paths we need to specially handle when we hit root
161 // Relative paths can't hit this path as driveStrLength == 0
162 if (driveStrLength > 0) {
163 if (lastSeparator < driveStrLength) {
164 if (path.length() > driveStrLength) {
165 String newPath = path.substring(0, driveStrLength);
166 return new LocalPath(newPath, driveStrLength, os);
167 } else {
168 return null;
169 }
170 }
171 } else {
172 if (lastSeparator == -1) {
173 if (!path.isEmpty()) {
174 return EMPTY;
175 } else {
176 return null;
177 }
178 }
179 }
180 String newPath = path.substring(0, lastSeparator);
181 return new LocalPath(newPath, driveStrLength, os);
182 }
183
184 /**
185 * Returns the {@link LocalPath} relative to the base {@link LocalPath}.
186 *
187 * <p>For example, <code>LocalPath.create("foo/bar/wiz").relativeTo(LocalPath.create("foo"))
188 * </code> returns <code>LocalPath.create("bar/wiz")</code>.
189 *
190 * <p>If the {@link LocalPath} is not a child of the passed {@link LocalPath} an {@link
191 * IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
192 * LocalPath} instances aren't both absolute or both relative.
193 */
194 public LocalPath relativeTo(LocalPath base) {
195 Preconditions.checkNotNull(base);
196 Preconditions.checkArgument(os == base.os);
197 if (isAbsolute() != base.isAbsolute()) {
198 throw new IllegalArgumentException(
199 "Cannot relativize an absolute and a non-absolute path pair");
200 }
201 String basePath = base.path;
202 if (!os.startsWith(path, basePath)) {
203 throw new IllegalArgumentException(
204 String.format("Path '%s' is not under '%s', cannot relativize", this, base));
205 }
206 int bn = basePath.length();
207 if (bn == 0) {
208 return this;
209 }
210 if (path.length() == bn) {
211 return EMPTY;
212 }
213 final int lastSlashIndex;
214 if (basePath.charAt(bn - 1) == '/') {
215 lastSlashIndex = bn - 1;
216 } else {
217 lastSlashIndex = bn;
218 }
219 if (path.charAt(lastSlashIndex) != '/') {
220 throw new IllegalArgumentException(
221 String.format("Path '%s' is not under '%s', cannot relativize", this, base));
222 }
223 String newPath = path.substring(lastSlashIndex + 1);
224 return new LocalPath(newPath, 0 /* Always a relative path */, os);
225 }
226
227 /**
228 * Splits a path into its constituent parts. The root is not included. This is an inefficient
229 * operation and should be avoided.
230 */
aehligc801c392017-12-19 07:12:25 -0800231 public String[] split() {
232 String[] segments = path.split("/");
233 if (driveStrLength > 0) {
234 // String#split("/") for some reason returns a zero-length array
235 // String#split("/hello") returns a 2-length array, so this makes little sense
236 if (segments.length == 0) {
237 return segments;
238 }
239 return Arrays.copyOfRange(segments, 1, segments.length);
tomlu4a2f2c52017-12-12 12:32:22 -0800240 }
241 return segments;
242 }
243
244 /**
245 * Returns whether this path is an ancestor of another path.
246 *
247 * <p>A path is considered an ancestor of itself.
248 *
249 * <p>An absolute path can never be an ancestor of a relative path, and vice versa.
250 */
251 public boolean startsWith(LocalPath other) {
252 Preconditions.checkNotNull(other);
253 Preconditions.checkArgument(os == other.os);
254 if (other.path.length() > path.length()) {
255 return false;
256 }
257 if (driveStrLength != other.driveStrLength) {
258 return false;
259 }
260 if (!os.startsWith(path, other.path)) {
261 return false;
262 }
263 return path.length() == other.path.length()
264 || other.path.length() == driveStrLength
265 || path.charAt(other.path.length()) == os.getSeparator();
266 }
267
268 public boolean isAbsolute() {
269 return driveStrLength > 0;
270 }
271
272 @Override
273 public String toString() {
274 return path;
275 }
276
277 @Override
278 public boolean equals(Object o) {
279 if (this == o) {
280 return true;
281 }
282 if (o == null || getClass() != o.getClass()) {
283 return false;
284 }
285 return os.compare(this.path, ((LocalPath) o).path) == 0;
286 }
287
288 @Override
289 public int hashCode() {
290 return os.hashPath(this.path);
291 }
292
293 @Override
294 public int compareTo(LocalPath o) {
295 return os.compare(this.path, o.path);
296 }
297
298 /**
299 * An interface class representing the differences in path style between different OSs.
300 *
301 * <p>Eg. case sensitivity, '/' mounts vs. 'C:/', etc.
302 */
303 @VisibleForTesting
304 interface OsPathPolicy {
305 int NORMALIZED = 0; // Path is normalized
306 int NEEDS_NORMALIZE = 1; // Path requires normalization
307
308 /** Returns required normalization level, passed to {@link #normalize}. */
309 int needsToNormalize(String path);
310
311 /**
312 * Normalizes the passed string according to the passed normalization level.
313 *
314 * @param normalizationLevel The normalizationLevel from {@link #needsToNormalize}
315 */
316 String normalize(String path, int normalizationLevel);
317
318 /**
319 * Returns the length of the mount, eg. 1 for unix '/', 3 for Windows 'C:/'.
320 *
321 * <p>If the path is relative, 0 is returned
322 */
323 int getDriveStrLength(String path);
324
325 /** Compares two path strings, using the given OS case sensitivity. */
326 int compare(String s1, String s2);
327
328 /** Computes the hash code for a path string. */
329 int hashPath(String s);
330
331 /**
332 * Returns whether the passed string starts with the given prefix, given the OS case
333 * sensitivity.
334 *
335 * <p>This is a pure string operation and doesn't need to worry about matching path segments.
336 */
337 boolean startsWith(String path, String prefix);
338
339 char getSeparator();
340
341 boolean isCaseSensitive();
342 }
343
344 @VisibleForTesting
345 static class UnixOsPathPolicy implements OsPathPolicy {
346
347 @Override
348 public int needsToNormalize(String path) {
349 int n = path.length();
350 int dotCount = 0;
351 char prevChar = 0;
352 for (int i = 0; i < n; i++) {
353 char c = path.charAt(i);
354 if (c == '/') {
355 if (prevChar == '/') {
356 return NEEDS_NORMALIZE;
357 }
358 if (dotCount == 1 || dotCount == 2) {
359 return NEEDS_NORMALIZE;
360 }
361 }
362 dotCount = c == '.' ? dotCount + 1 : 0;
363 prevChar = c;
364 }
aehligc801c392017-12-19 07:12:25 -0800365 if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
tomlu4a2f2c52017-12-12 12:32:22 -0800366 return NEEDS_NORMALIZE;
367 }
368 return NORMALIZED;
369 }
370
371 @Override
372 public String normalize(String path, int normalizationLevel) {
373 if (normalizationLevel == NORMALIZED) {
374 return path;
375 }
376 if (path.isEmpty()) {
377 return path;
378 }
379 boolean isAbsolute = path.charAt(0) == '/';
aehligc801c392017-12-19 07:12:25 -0800380 String[] segments = path.split("/+");
381 int segmentCount = removeRelativePaths(segments, isAbsolute ? 1 : 0);
tomlu4a2f2c52017-12-12 12:32:22 -0800382 StringBuilder sb = new StringBuilder(path.length());
383 if (isAbsolute) {
384 sb.append('/');
385 }
386 for (int i = 0; i < segmentCount; ++i) {
387 sb.append(segments[i]);
388 sb.append('/');
389 }
390 if (segmentCount > 0) {
391 sb.deleteCharAt(sb.length() - 1);
392 }
393 return sb.toString();
394 }
395
396 @Override
397 public int getDriveStrLength(String path) {
398 if (path.length() == 0) {
399 return 0;
400 }
401 return (path.charAt(0) == '/') ? 1 : 0;
402 }
403
404 @Override
405 public int compare(String s1, String s2) {
406 return s1.compareTo(s2);
407 }
408
409 @Override
410 public int hashPath(String s) {
411 return s.hashCode();
412 }
413
414 @Override
415 public boolean startsWith(String path, String prefix) {
416 return path.startsWith(prefix);
417 }
418
419 @Override
420 public char getSeparator() {
421 return '/';
422 }
423
424 @Override
425 public boolean isCaseSensitive() {
426 return true;
427 }
428 }
429
430 /** Mac is a unix file system that is case insensitive. */
431 @VisibleForTesting
432 static class MacOsPathPolicy extends UnixOsPathPolicy {
433 @Override
434 public int compare(String s1, String s2) {
435 return s1.compareToIgnoreCase(s2);
436 }
437
438 @Override
439 public int hashPath(String s) {
440 return s.toLowerCase().hashCode();
441 }
442
443 @Override
444 public boolean isCaseSensitive() {
445 return false;
446 }
447 }
448
449 @VisibleForTesting
450 static class WindowsOsPathPolicy implements OsPathPolicy {
451
452 private static final int NEEDS_SHORT_PATH_NORMALIZATION = NEEDS_NORMALIZE + 1;
453
aehligc801c392017-12-19 07:12:25 -0800454 // msys root, used to resolve paths from msys starting with "/"
455 private static final AtomicReference<String> UNIX_ROOT = new AtomicReference<>(null);
tomlu4a2f2c52017-12-12 12:32:22 -0800456 private final ShortPathResolver shortPathResolver;
457
458 interface ShortPathResolver {
459 String resolveShortPath(String path);
460 }
461
462 static class DefaultShortPathResolver implements ShortPathResolver {
463 @Override
464 public String resolveShortPath(String path) {
465 try {
466 return WindowsFileOperations.getLongPath(path);
467 } catch (IOException e) {
468 return path;
469 }
470 }
471 }
472
473 WindowsOsPathPolicy() {
474 this(new DefaultShortPathResolver());
475 }
476
477 WindowsOsPathPolicy(ShortPathResolver shortPathResolver) {
478 this.shortPathResolver = shortPathResolver;
479 }
480
481 @Override
482 public int needsToNormalize(String path) {
483 int n = path.length();
484 int normalizationLevel = 0;
aehligc801c392017-12-19 07:12:25 -0800485 // Check for unix path
486 if (n > 0 && path.charAt(0) == '/') {
487 normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
488 }
tomlu4a2f2c52017-12-12 12:32:22 -0800489 int dotCount = 0;
490 char prevChar = 0;
491 int segmentBeginIndex = 0; // The start index of the current path index
492 boolean segmentHasShortPathChar = false; // Triggers more expensive short path regex test
493 for (int i = 0; i < n; i++) {
494 char c = path.charAt(i);
495 if (c == '/' || c == '\\') {
496 if (c == '\\') {
497 normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
498 }
499 // No need to check for '\' here because that already causes normalization
500 if (prevChar == '/') {
501 normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
502 }
503 if (dotCount == 1 || dotCount == 2) {
504 normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
505 }
506 if (segmentHasShortPathChar) {
507 if (WindowsShortPath.isShortPath(path.substring(segmentBeginIndex, i))) {
508 normalizationLevel = Math.max(normalizationLevel, NEEDS_SHORT_PATH_NORMALIZATION);
509 }
510 }
511 segmentBeginIndex = i + 1;
512 segmentHasShortPathChar = false;
513 } else if (c == '~') {
514 // This path segment might be a Windows short path segment
515 segmentHasShortPathChar = true;
516 }
517 dotCount = c == '.' ? dotCount + 1 : 0;
518 prevChar = c;
519 }
aehligc801c392017-12-19 07:12:25 -0800520 if (prevChar == '/' || dotCount == 1 || dotCount == 2) {
tomlu4a2f2c52017-12-12 12:32:22 -0800521 normalizationLevel = Math.max(normalizationLevel, NEEDS_NORMALIZE);
522 }
523 return normalizationLevel;
524 }
525
526 @Override
527 public String normalize(String path, int normalizationLevel) {
528 if (normalizationLevel == NORMALIZED) {
529 return path;
530 }
531 if (normalizationLevel == NEEDS_SHORT_PATH_NORMALIZATION) {
532 String resolvedPath = shortPathResolver.resolveShortPath(path);
533 if (resolvedPath != null) {
534 path = resolvedPath;
535 }
536 }
aehligc801c392017-12-19 07:12:25 -0800537 String[] segments = path.split("[\\\\/]+");
tomlu4a2f2c52017-12-12 12:32:22 -0800538 int driveStrLength = getDriveStrLength(path);
539 boolean isAbsolute = driveStrLength > 0;
aehligc801c392017-12-19 07:12:25 -0800540 int segmentSkipCount = isAbsolute ? 1 : 0;
tomlu4a2f2c52017-12-12 12:32:22 -0800541
542 StringBuilder sb = new StringBuilder(path.length());
543 if (isAbsolute) {
aehligc801c392017-12-19 07:12:25 -0800544 char driveLetter = path.charAt(0);
545 sb.append(Character.toUpperCase(driveLetter));
546 sb.append(":/");
547 }
548 // unix path support
549 if (!path.isEmpty() && path.charAt(0) == '/') {
550 if (path.length() == 2 || (path.length() > 2 && path.charAt(2) == '/')) {
551 sb.append(Character.toUpperCase(path.charAt(1)));
tomlu82e68b72017-12-14 12:51:10 -0800552 sb.append(":/");
aehligc801c392017-12-19 07:12:25 -0800553 segmentSkipCount = 2;
554 } else {
555 String unixRoot = getUnixRoot();
556 sb.append(unixRoot);
tomlu4a2f2c52017-12-12 12:32:22 -0800557 }
558 }
559 int segmentCount = removeRelativePaths(segments, segmentSkipCount);
560 for (int i = 0; i < segmentCount; ++i) {
561 sb.append(segments[i]);
562 sb.append('/');
563 }
564 if (segmentCount > 0) {
565 sb.deleteCharAt(sb.length() - 1);
566 }
567 return sb.toString();
568 }
569
570 @Override
571 public int getDriveStrLength(String path) {
572 int n = path.length();
573 if (n < 3) {
574 return 0;
575 }
576 if (isDriveLetter(path.charAt(0))
577 && path.charAt(1) == ':'
578 && (path.charAt(2) == '/' || path.charAt(2) == '\\')) {
579 return 3;
580 }
581 return 0;
582 }
583
584 private static boolean isDriveLetter(char c) {
585 return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
586 }
587
588 @Override
589 public int compare(String s1, String s2) {
590 // Windows is case-insensitive
591 return s1.compareToIgnoreCase(s2);
592 }
593
594 @Override
595 public int hashPath(String s) {
596 // Windows is case-insensitive
597 return s.toLowerCase().hashCode();
598 }
599
600 @Override
601 public boolean startsWith(String path, String prefix) {
602 int pathn = path.length();
603 int prefixn = prefix.length();
604 if (pathn < prefixn) {
605 return false;
606 }
607 for (int i = 0; i < prefixn; ++i) {
608 if (Character.toLowerCase(path.charAt(i)) != Character.toLowerCase(prefix.charAt(i))) {
609 return false;
610 }
611 }
612 return true;
613 }
614
615 @Override
616 public char getSeparator() {
617 return '/';
618 }
619
620 @Override
621 public boolean isCaseSensitive() {
622 return false;
623 }
624
aehligc801c392017-12-19 07:12:25 -0800625 private String getUnixRoot() {
626 String value = UNIX_ROOT.get();
627 if (value == null) {
628 String jvmFlag = "bazel.windows_unix_root";
629 value = determineUnixRoot(jvmFlag);
630 if (value == null) {
631 throw new IllegalStateException(
632 String.format(
633 "\"%1$s\" JVM flag is not set. Use the --host_jvm_args flag or export the "
634 + "BAZEL_SH environment variable. For example "
635 + "\"--host_jvm_args=-D%1$s=c:/tools/msys64\" or "
636 + "\"set BAZEL_SH=c:/tools/msys64/usr/bin/bash.exe\".",
637 jvmFlag));
638 }
639 if (getDriveStrLength(value) != 3) {
640 throw new IllegalStateException(
641 String.format("\"%s\" must be an absolute path, got: \"%s\"", jvmFlag, value));
642 }
643 value = value.replace('\\', '/');
644 if (value.length() > 3 && value.endsWith("/")) {
645 value = value.substring(0, value.length() - 1);
646 }
647 UNIX_ROOT.set(value);
648 }
649 return value;
650 }
651
652 private String determineUnixRoot(String jvmArgName) {
653 // Get the path from a JVM flag, if specified.
654 String path = System.getProperty(jvmArgName);
655 if (path == null) {
656 return null;
657 }
658 path = path.trim();
659 if (path.isEmpty()) {
660 return null;
661 }
662 return path;
tomlu4a2f2c52017-12-12 12:32:22 -0800663 }
664 }
665
666 private static OsPathPolicy createFilePathOs() {
667 switch (OS.getCurrent()) {
668 case LINUX:
669 case FREEBSD:
670 case UNKNOWN:
671 return new UnixOsPathPolicy();
672 case DARWIN:
673 return new MacOsPathPolicy();
674 case WINDOWS:
675 return new WindowsOsPathPolicy();
676 default:
677 throw new AssertionError("Not covering all OSs");
678 }
679 }
680
681 /**
682 * Normalizes any '.' and '..' in-place in the segment array by shifting other segments to the
683 * front. Returns the remaining number of items.
684 */
685 private static int removeRelativePaths(String[] segments, int starti) {
686 int segmentCount = 0;
687 int shift = starti;
aehligc801c392017-12-19 07:12:25 -0800688 for (int i = starti; i < segments.length; ++i) {
tomlu4a2f2c52017-12-12 12:32:22 -0800689 String segment = segments[i];
690 switch (segment) {
691 case ".":
692 // Just discard it
693 ++shift;
694 break;
695 case "..":
696 if (segmentCount > 0 && !segments[segmentCount - 1].equals("..")) {
697 // Remove the last segment, if there is one and it is not "..". This
698 // means that the resulting path can still contain ".."
699 // segments at the beginning.
700 segmentCount--;
701 shift += 2;
702 break;
703 }
704 // Fall through
705 default:
706 ++segmentCount;
707 if (shift > 0) {
708 segments[i - shift] = segments[i];
709 }
710 break;
711 }
712 }
713 return segmentCount;
714 }
715}