/* * Copyright 2013 Google Inc. * * 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.common.jimfs; import java.nio.file.InvalidPathException; import java.util.Iterator; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.checkerframework.checker.nullness.compatqual.NullableDecl; /** * Windows-style path type. * * @author Colin Decker */ final class WindowsPathType extends PathType { /** Windows path type. */ static final WindowsPathType INSTANCE = new WindowsPathType(); /** * Matches the C:foo\bar path format, which has a root (C:) and names (foo\bar) and matches a path * relative to the working directory on that drive. Currently can't support that format as it * requires behavior that differs completely from Unix. */ // TODO(cgdecker): Can probably support this at some point // It would require: // - A method like PathType.isAbsolute(Path) or something to that effect; this would allow // WindowsPathType to distinguish between an absolute root path (C:\) and a relative root // path (C:) // - Special handling for relative paths that have a root. This handling would determine the // root directory and then determine the working directory from there. The file system would // still have one working directory; for the root that working directory is under, it is the // working directory. For every other root, the root itself is the working directory. private static final Pattern WORKING_DIR_WITH_DRIVE = Pattern.compile("^[a-zA-Z]:([^\\\\].*)?$"); /** Pattern for matching trailing spaces in file names. */ private static final Pattern TRAILING_SPACES = Pattern.compile("[ ]+(\\\\|$)"); private WindowsPathType() { super(true, '\\', '/'); } @Override public ParseResult parsePath(String path) { String original = path; path = path.replace('/', '\\'); if (WORKING_DIR_WITH_DRIVE.matcher(path).matches()) { throw new InvalidPathException( original, "Jimfs does not currently support the Windows syntax for a relative path " + "on a specific drive (e.g. \"C:foo\\bar\")"); } String root; if (path.startsWith("\\\\")) { root = parseUncRoot(path, original); } else if (path.startsWith("\\")) { throw new InvalidPathException( original, "Jimfs does not currently support the Windows syntax for an absolute path " + "on the current drive (e.g. \"\\foo\\bar\")"); } else { root = parseDriveRoot(path); } // check for root.length() > 3 because only "C:\" type roots are allowed to have : int startIndex = root == null || root.length() > 3 ? 0 : root.length(); for (int i = startIndex; i < path.length(); i++) { char c = path.charAt(i); if (isReserved(c)) { throw new InvalidPathException(original, "Illegal char <" + c + ">", i); } } Matcher trailingSpaceMatcher = TRAILING_SPACES.matcher(path); if (trailingSpaceMatcher.find()) { throw new InvalidPathException(original, "Trailing char < >", trailingSpaceMatcher.start()); } if (root != null) { path = path.substring(root.length()); if (!root.endsWith("\\")) { root = root + "\\"; } } return new ParseResult(root, splitter().split(path)); } /** Pattern for matching UNC \\host\share root syntax. */ private static final Pattern UNC_ROOT = Pattern.compile("^(\\\\\\\\)([^\\\\]+)?(\\\\[^\\\\]+)?"); /** * Parse the root of a UNC-style path, throwing an exception if the path does not start with a * valid UNC root. */ private String parseUncRoot(String path, String original) { Matcher uncMatcher = UNC_ROOT.matcher(path); if (uncMatcher.find()) { String host = uncMatcher.group(2); if (host == null) { throw new InvalidPathException(original, "UNC path is missing hostname"); } String share = uncMatcher.group(3); if (share == null) { throw new InvalidPathException(original, "UNC path is missing sharename"); } return path.substring(uncMatcher.start(), uncMatcher.end()); } else { // probably shouldn't ever reach this throw new InvalidPathException(original, "Invalid UNC path"); } } /** Pattern for matching normal C:\ drive letter root syntax. */ private static final Pattern DRIVE_LETTER_ROOT = Pattern.compile("^[a-zA-Z]:\\\\"); /** Parses a normal drive-letter root, e.g. "C:\". */ @NullableDecl private String parseDriveRoot(String path) { Matcher drivePathMatcher = DRIVE_LETTER_ROOT.matcher(path); if (drivePathMatcher.find()) { return path.substring(drivePathMatcher.start(), drivePathMatcher.end()); } return null; } /** Checks if c is one of the reserved characters that aren't allowed in Windows file names. */ private static boolean isReserved(char c) { switch (c) { case '<': case '>': case ':': case '"': case '|': case '?': case '*': return true; default: return c <= 31; } } @Override public String toString(@NullableDecl String root, Iterable<String> names) { StringBuilder builder = new StringBuilder(); if (root != null) { builder.append(root); } joiner().appendTo(builder, names); return builder.toString(); } @Override public String toUriPath(String root, Iterable<String> names, boolean directory) { if (root.startsWith("\\\\")) { root = root.replace('\\', '/'); } else { root = "/" + root.replace('\\', '/'); } StringBuilder builder = new StringBuilder(); builder.append(root); Iterator<String> iter = names.iterator(); if (iter.hasNext()) { builder.append(iter.next()); while (iter.hasNext()) { builder.append('/').append(iter.next()); } } if (directory && builder.charAt(builder.length() - 1) != '/') { builder.append('/'); } return builder.toString(); } @Override public ParseResult parseUriPath(String uriPath) { uriPath = uriPath.replace('/', '\\'); if (uriPath.charAt(0) == '\\' && uriPath.charAt(1) != '\\') { // non-UNC path, so the leading / was just there for the URI path format and isn't part // of what should be parsed uriPath = uriPath.substring(1); } return parsePath(uriPath); } }