diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java index ebbfd68a3..d823d23da 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/WithContainerStep.java @@ -73,34 +73,68 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +/** + * The type With container step. + */ public class WithContainerStep extends AbstractStepImpl { - + private static final Logger LOGGER = Logger.getLogger(WithContainerStep.class.getName()); private final @Nonnull String image; private String args; private String toolName; + private static boolean needToContainerizePath = false; + /** + * Instantiates a new With container step. + * + * @param image the image + */ @DataBoundConstructor public WithContainerStep(@Nonnull String image) { this.image = image; } - + + /** + * Gets image. + * + * @return the image + */ public String getImage() { return image; } + /** + * Sets args. + * + * @param args the args + */ @DataBoundSetter public void setArgs(String args) { this.args = Util.fixEmpty(args); } + /** + * Gets args. + * + * @return the args + */ public String getArgs() { return args; } + /** + * Gets tool name. + * + * @return the tool name + */ public String getToolName() { return toolName; } + /** + * Sets tool name. + * + * @param toolName the tool name + */ @DataBoundSetter public void setToolName(String toolName) { this.toolName = Util.fixEmpty(toolName); } @@ -109,7 +143,10 @@ private static void destroy(String container, Launcher launcher, Node node, EnvV new DockerClient(launcher, node, toolName).stop(launcherEnv, container); } - // TODO switch to GeneralNonBlockingStepExecution + /** + * The type Execution. + */ +// TODO switch to GeneralNonBlockingStepExecution public static class Execution extends AbstractStepExecutionImpl { private static final long serialVersionUID = 1; @Inject(optional=true) private transient WithContainerStep step; @@ -124,6 +161,9 @@ public static class Execution extends AbstractStepExecutionImpl { private String container; private String toolName; + /** + * Instantiates a new Execution. + */ public Execution() { } @@ -144,6 +184,14 @@ public Execution() { ? new DockerClient(launcher, node, toolName) : new WindowsDockerClient(launcher, node, toolName); + String containerOsType = dockerClient.inspect(new EnvVars(), step.image, ".Os"); + + if (!launcher.isUnix() && containerOsType != null && containerOsType.equalsIgnoreCase("linux")) { + needToContainerizePath = true; + dockerClient.setNeedToContainerizePath(true); + dockerClient.setContainerUnix(true); + } + VersionNumber dockerVersion = dockerClient.version(); if (dockerVersion != null) { if (dockerVersion.isOlderThan(new VersionNumber("1.7"))) { @@ -194,7 +242,7 @@ public Execution() { volumes.put(tmp, tmp); } - String command = launcher.isUnix() ? "cat" : "cmd.exe"; + String command = dockerClient.runCommand(); container = dockerClient.run(env, step.image, step.args, ws, volumes, volumesFromContainers, envReduced, dockerClient.whoAmI(), /* expected to hang until killed */ command); final List ps = dockerClient.listProcess(env, container); if (!ps.contains(command)) { @@ -207,9 +255,9 @@ public Execution() { ImageAction.add(step.image, run); getContext().newBodyInvoker(). - withContext(BodyInvoker.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), new Decorator(container, envHost, ws, toolName, dockerVersion))). - withCallback(new Callback(container, toolName)). - start(); + withContext(BodyInvoker.mergeLauncherDecorators(getContext().get(LauncherDecorator.class), new Decorator(container, envHost, ws, toolName, dockerVersion))). + withCallback(new Callback(container, toolName)). + start(); return false; } @@ -246,6 +294,15 @@ private static class Decorator extends LauncherDecorator implements Serializable private final boolean hasEnv; private final boolean hasWorkdir; + /** + * Instantiates a new Decorator. + * + * @param container the container + * @param envHost the env host + * @param ws the ws + * @param toolName the tool name + * @param dockerVersion the docker version + */ Decorator(String container, EnvVars envHost, String ws, String toolName, VersionNumber dockerVersion) { this.container = container; this.envHost = Util.mapToEnv(envHost); @@ -311,8 +368,8 @@ private static class Decorator extends LauncherDecorator implements Serializable masksPrefixList.add(false); prefix.addAll(envReduced); masksPrefixList.addAll(envReduced.stream() - .map(v -> true) - .collect(Collectors.toList())); + .map(v -> true) + .collect(Collectors.toList())); } boolean[] originalMasks = starter.masks(); @@ -332,8 +389,17 @@ private static class Decorator extends LauncherDecorator implements Serializable System.arraycopy(originalMasks, 0, masks, prefix.size(), originalMasks.length); starter.masks(masks); + if (needToContainerizePath && ws != null) { + String wsTrimmed = ws.replaceAll("[/\\\\]+$", ""); + List cmds = starter.cmds(); + for (int i = 0; i < cmds.size(); i++) { + cmds.set(i, WindowsDockerClient.containerizePath(cmds.get(i), wsTrimmed)); + } + } + return super.launch(starter); } + @Override public void kill(Map modelEnvVars) throws IOException, InterruptedException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); String executable = getExecutable(); @@ -382,6 +448,12 @@ private static class Callback extends BodyExecutionCallback.TailCall { private final String container; private final String toolName; + /** + * Instantiates a new Callback. + * + * @param container the container + * @param toolName the tool name + */ Callback(String container, String toolName) { this.container = container; this.toolName = toolName; @@ -393,8 +465,14 @@ private static class Callback extends BodyExecutionCallback.TailCall { } + /** + * The type Descriptor. + */ @Extension public static class DescriptorImpl extends AbstractStepDescriptorImpl { + /** + * Instantiates a new Descriptor. + */ public DescriptorImpl() { super(Execution.class); } diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java index 336efa0bb..6630858d8 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java @@ -62,7 +62,7 @@ /** * Simple docker client for Pipeline. - * + * * @author tom.fennelly@gmail.com */ public class DockerClient { @@ -83,13 +83,25 @@ public class DockerClient { @Restricted(NoExternalUse.class) public static boolean SKIP_RM_ON_STOP = Boolean.getBoolean(DockerClient.class.getName() + ".SKIP_RM_ON_STOP"); - // e.g. 2015-04-09T13:40:21.981801679Z + /** + * The constant DOCKER_DATE_TIME_FORMAT. + */ +// e.g. 2015-04-09T13:40:21.981801679Z public static final String DOCKER_DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; - + private final Launcher launcher; private final @CheckForNull Node node; private final @CheckForNull String toolName; + private boolean needToContainerizePath = false; + private boolean isContainerUnix = true; + /** + * Instantiates a new Docker client. + * + * @param launcher the launcher + * @param node the node + * @param toolName the tool name + */ public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) { this.launcher = launcher; this.node = node; @@ -99,16 +111,18 @@ public DockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckF /** * Run a docker image. * - * @param launchEnv Docker client launch environment. - * @param image The image name. - * @param args Any additional arguments for the {@code docker run} command. - * @param workdir The working directory in the container, or {@code null} for default. - * @param volumes Volumes to be bound. Supply an empty list if no volumes are to be bound. + * @param launchEnv Docker client launch environment. + * @param image The image name. + * @param args Any additional arguments for the {@code docker run} command. + * @param workdir The working directory in the container, or {@code null} for default. + * @param volumes Volumes to be bound. Supply an empty list if no volumes are to be bound. * @param volumesFromContainers Mounts all volumes from the given containers. - * @param containerEnv Environment variables to set in container. - * @param user The uid:gid to execute the container command as. Use {@link #whoAmI()}. - * @param command The command to execute in the image container being run. + * @param containerEnv Environment variables to set in container. + * @param user The uid:gid to execute the container command as. Use {@link #whoAmI()}. + * @param command The command to execute in the image container being run. * @return The container ID. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNull String args, @CheckForNull String workdir, @Nonnull Map volumes, @Nonnull Collection volumesFromContainers, @Nonnull EnvVars containerEnv, @Nonnull String user, @Nonnull String... command) throws IOException, InterruptedException { ArgumentListBuilder argb = new ArgumentListBuilder(); @@ -122,7 +136,7 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu if (args != null) { argb.addTokenized(args); } - + if (workdir != null) { argb.add("-w", workdir); } @@ -146,6 +160,15 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu } } + /** + * List process list. + * + * @param launchEnv the launch env + * @param containerId the container id + * @return the list + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { LaunchResult result = launch(launchEnv, false, "top", containerId, "-eo", "pid,comm"); if (result.getStatus() != 0) { @@ -170,13 +193,15 @@ public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String cont /** * Stop a container. - * - *

+ * + *

* Also removes ({@link #rm(EnvVars, String)}) the container if property * SKIP_RM_ON_STOP is unset or equals false. - * - * @param launchEnv Docker client launch environment. + * + * @param launchEnv Docker client launch environment. * @param containerId The container ID. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public void stop(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { LaunchResult result = launch(launchEnv, false, "stop", "--time=1", containerId); @@ -190,9 +215,11 @@ public void stop(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws /** * Remove a container. - * - * @param launchEnv Docker client launch environment. + * + * @param launchEnv Docker client launch environment. * @param containerId The container ID. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { LaunchResult result; @@ -204,10 +231,13 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I /** * Inspect a docker image/container. + * * @param launchEnv Docker client launch environment. - * @param objectId The image/container ID. + * @param objectId The image/container ID. * @param fieldPath The data path of the data required e.g. {@code .NetworkSettings.IPAddress}. * @return The inspected field value. Null if the command failed + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public @CheckForNull String inspect(@Nonnull EnvVars launchEnv, @Nonnull String objectId, @Nonnull String fieldPath) throws IOException, InterruptedException { LaunchResult result = launch(launchEnv, true, "inspect", "-f", String.format("{{%s}}", fieldPath), objectId); @@ -217,26 +247,27 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I return null; } } - + /** * Inspect a docker image/container. + * * @param launchEnv Docker client launch environment. - * @param objectId The image/container ID. + * @param objectId The image/container ID. * @param fieldPath The data path of the data required e.g. {@code .NetworkSettings.IPAddress}. * @return The inspected field value. May be an empty string - * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request + * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request * @throws InterruptedException Interrupted * @since 1.1 */ - public @Nonnull String inspectRequiredField(@Nonnull EnvVars launchEnv, @Nonnull String objectId, - @Nonnull String fieldPath) throws IOException, InterruptedException { + public @Nonnull String inspectRequiredField(@Nonnull EnvVars launchEnv, @Nonnull String objectId, + @Nonnull String fieldPath) throws IOException, InterruptedException { final String fieldValue = inspect(launchEnv, objectId, fieldPath); if (fieldValue == null) { throw new IOException("Cannot retrieve " + fieldPath + " from 'docker inspect " + objectId + "'"); } return fieldValue; } - + private @CheckForNull Date getCreatedDate(@Nonnull EnvVars launchEnv, @Nonnull String objectId) throws IOException, InterruptedException { String createdString = inspect(launchEnv, objectId, "json .Created"); if (createdString == null) { @@ -254,8 +285,9 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I /** * Get the docker version. * - * @return The {@link VersionNumber} instance if the version string matches the expected format, - * otherwise {@code null}. + * @return The {@link VersionNumber} instance if the version string matches the expected format, otherwise {@code null}. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public @CheckForNull VersionNumber version() throws IOException, InterruptedException { LaunchResult result = launch(new EnvVars(), true, "-v"); @@ -265,13 +297,14 @@ public void rm(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws I return null; } } - + private static final Pattern pattern = Pattern.compile("^(\\D+)(\\d+)\\.(\\d+)\\.(\\d+)(.*)"); + /** * Parse a Docker version string (e.g. "Docker version 1.5.0, build a8a31ef"). + * * @param versionString The version string to parse. - * @return The {@link VersionNumber} instance if the version string matched the - * expected format, otherwise {@code null}. + * @return The {@link VersionNumber} instance if the version string matched the expected format, otherwise {@code null}. */ protected static VersionNumber parseVersionNumber(@Nonnull String versionString) { Matcher matcher = pattern.matcher(versionString.trim()); @@ -282,7 +315,7 @@ protected static VersionNumber parseVersionNumber(@Nonnull String versionString) return new VersionNumber(String.format("%s.%s.%s", major, minor, maint)); } else { return null; - } + } } private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, @Nonnull String... args) throws IOException, InterruptedException { @@ -319,6 +352,8 @@ private LaunchResult launch(@Nonnull EnvVars launchEnv, boolean quiet, FilePath * Who is executing this {@link DockerClient} instance. * * @return a {@link String} containing the uid:gid. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception */ public String whoAmI() throws IOException, InterruptedException { if (!launcher.isUnix()) { @@ -340,8 +375,9 @@ public String whoAmI() throws IOException, InterruptedException { * Checks if this {@link DockerClient} instance is running inside a container and returns the id of the container * if so. * - * @return an optional string containing the container id, or absent if - * it isn't containerized. + * @return an optional string containing the container id, or absent if it isn't containerized. + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception * @see Discussion */ public Optional getContainerIdIfContainerized() throws IOException, InterruptedException { @@ -355,6 +391,15 @@ public Optional getContainerIdIfContainerized() throws IOException, Inte return ControlGroup.getContainerId(cgroupFile); } + /** + * Gets container record. + * + * @param launchEnv the launch env + * @param containerId the container id + * @return the container record + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ public ContainerRecord getContainerRecord(@Nonnull EnvVars launchEnv, String containerId) throws IOException, InterruptedException { String host = inspectRequiredField(launchEnv, containerId, ".Config.Hostname"); String containerName = inspectRequiredField(launchEnv, containerId, ".Name"); @@ -363,17 +408,18 @@ public ContainerRecord getContainerRecord(@Nonnull EnvVars launchEnv, String con // TODO get tags and add for ContainerRecord return new ContainerRecord(host, containerId, image, containerName, - (created != null ? created.getTime() : 0L), - Collections.emptyMap()); + (created != null ? created.getTime() : 0L), + Collections.emptyMap()); } /** * Inspect the mounts of a container. * These might have been declared {@code VOLUME}s, or mounts defined via {@code --volume}. - * @param launchEnv Docker client launch environment. + * + * @param launchEnv Docker client launch environment. * @param containerID The container ID. * @return a list of filesystem paths inside the container - * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request + * @throws IOException Execution error. Also fails if cannot retrieve the requested field from the request * @throws InterruptedException Interrupted */ public List getVolumes(@Nonnull EnvVars launchEnv, String containerID) throws IOException, InterruptedException { @@ -388,4 +434,50 @@ public List getVolumes(@Nonnull EnvVars launchEnv, String containerID) t } return Arrays.asList(volumes.replace("\\", "/").split("\\n")); } + + /** + * Run command string. + * + * @return the string + */ + public String runCommand() { + return "cat"; + } + + /** + * Is need to containerize path boolean. + * + * @return the boolean + */ + public boolean isNeedToContainerizePath() { + return needToContainerizePath; + } + + /** + * Sets need to containerize path. + * + * @param needToContainerizePath the need to containerize path + */ + public void setNeedToContainerizePath(boolean needToContainerizePath) { + this.needToContainerizePath = needToContainerizePath; + } + + /** + * Is container unix boolean. + * + * @return the boolean + */ + public boolean isContainerUnix() { + return isContainerUnix; + } + + /** + * Sets container unix. + * + * @param containerUnix the container unix + */ + public void setContainerUnix(boolean containerUnix) { + isContainerUnix = containerUnix; + } + } diff --git a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java index 2de11c25d..97a3ab681 100644 --- a/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java +++ b/src/main/java/org/jenkinsci/plugins/docker/workflow/client/WindowsDockerClient.java @@ -17,12 +17,24 @@ import java.util.logging.Level; import java.util.logging.Logger; +/** + * The type Windows docker client. + */ public class WindowsDockerClient extends DockerClient { private static final Logger LOGGER = Logger.getLogger(WindowsDockerClient.class.getName()); private final Launcher launcher; private final Node node; + private boolean needToContainerizePath = false; + private boolean isContainerUnix = false; + /** + * Instantiates a new Windows docker client. + * + * @param launcher the launcher + * @param node the node + * @param toolName the tool name + */ public WindowsDockerClient(@Nonnull Launcher launcher, @CheckForNull Node node, @CheckForNull String toolName) { super(launcher, node, toolName); this.launcher = launcher; @@ -37,10 +49,10 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu } if (workdir != null) { - argb.add("-w", workdir); + argb.add("-w", containerizePathIfNeeded(workdir)); } for (Map.Entry volume : volumes.entrySet()) { - argb.add("-v", volume.getKey() + ":" + volume.getValue()); + argb.add("-v", volume.getKey() + ":" + containerizePathIfNeeded(volume.getValue())); } for (String containerId : volumesFromContainers) { argb.add("--volumes-from", containerId); @@ -61,6 +73,9 @@ public String run(@Nonnull EnvVars launchEnv, @Nonnull String image, @CheckForNu @Override public List listProcess(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { + if (isContainerUnix) { + return listProcessUnixContainer(launchEnv, containerId); + } LaunchResult result = launch(launchEnv, false, null, "docker", "top", containerId); if (result.getStatus() != 0) { throw new IOException(String.format("Failed to run top '%s'. Error: %s", containerId, result.getErr())); @@ -135,4 +150,197 @@ private LaunchResult launch(EnvVars env, boolean quiet, FilePath workDir, Argume return result; } + + /** + * List process unix container list. + * + * @param launchEnv the launch env + * @param containerId the container id + * @return the list + * @throws IOException the io exception + * @throws InterruptedException the interrupted exception + */ + public List listProcessUnixContainer(@Nonnull EnvVars launchEnv, @Nonnull String containerId) throws IOException, InterruptedException { + LaunchResult result = launch(launchEnv, false, null, "docker", "top", containerId); + if (result.getStatus() != 0) { + throw new IOException(String.format("Failed to run top '%s'. Error: %s", containerId, result.getErr())); + } + List processes = new ArrayList<>(); + try (Reader r = new StringReader(result.getOut()); + BufferedReader in = new BufferedReader(r)) { + String line; + in.readLine(); // ps header + while ((line = in.readLine()) != null) { + final StringTokenizer stringTokenizer = new StringTokenizer(line, " "); + if (stringTokenizer.countTokens() < 4) { + throw new IOException("Unexpected `docker top` output : " + line); + } + stringTokenizer.nextToken(); // PID + stringTokenizer.nextToken(); // USER + stringTokenizer.nextToken(); // TIME + processes.add(stringTokenizer.nextToken()); // COMMAND + } + } + return processes; + } + + /** + * @return command to run as entry-point + */ + @Override + public String runCommand() { + if (isContainerUnix) { + return "cat"; + } + return "cmd"; + } + + /** + * @return boolean path need to be containerize + */ + public boolean isNeedToContainerizePath() { + return needToContainerizePath; + } + + /** + * @param needToContainerizePath the need to containerize path + */ + public void setNeedToContainerizePath(boolean needToContainerizePath) { + this.needToContainerizePath = needToContainerizePath; + } + + + /** + * @return boolean container type (unix or windows) + */ + public boolean isContainerUnix() { + return isContainerUnix; + } + + /** + * @param containerUnix the container unix + */ + public void setContainerUnix(boolean containerUnix) { + isContainerUnix = containerUnix; + } + + /** + * Containerize path if needed string. + * + * @param path the path + * @return the string + */ + public String containerizePathIfNeeded(String path) { + return containerizePathIfNeeded(path, null); + } + + /** + * Containerize path if needed string. + * + * @param path the path + * @param prefix the prefix + * @return the string + */ + public String containerizePathIfNeeded(String path, String prefix) { + if (this.needToContainerizePath) + return WindowsDockerClient.containerizePath(path, prefix); + return path; + } + + /** + * Containerize path string. + * + * @param path the path + * @param prefix the prefix + * @return the string + */ + public static String containerizePath(String path, String prefix) { + StringBuffer result = new StringBuffer(); + char[] pathChars = path.toCharArray(); + char[] prefixChars = (prefix == null) ? null : prefix.toCharArray(); + + for (int i = 0; i < pathChars.length; i++) { + char currentChar = pathChars[i]; + if (currentChar == ':' && i > 0 && i < pathChars.length - 1) { + char previousChar = pathChars[i - 1]; + if ((previousChar >= 'a' && previousChar <= 'z') || (previousChar >= 'A' && previousChar <= 'Z')) { + char nextChar = pathChars[i + 1]; + if (nextChar == '/' || nextChar == '\\') { + char nextNextChar = (i < pathChars.length - 2) ? pathChars[i + 2] : ' '; + if (nextNextChar != '/') { + if (prefix == null || checkPrefix(pathChars, i - 1, prefixChars)) { + result.setCharAt(i - 1, '/'); + result.append(Character.toLowerCase(previousChar)); + result.append('/'); + i++; + i++; + + boolean done = false; + for (; i < pathChars.length; i++) { + currentChar = pathChars[i]; + switch (currentChar) { + case '\\': + result.append('/'); + break; + + case '?': + case '<': + case '>': + case ':': + case '*': + case '|': + case '"': + case '\'': + result.append(currentChar); + done = true; + break; + + default: + result.append(currentChar); + break; + } + + if (done) + break; + } + + continue; + } + } + } + } + } + + result.append(currentChar); + + } + return result.toString(); + + } + + /** + * @param pathChars + * @param index + * @param prefixChars + * @return + */ + private static boolean checkPrefix(char[] pathChars, int index, char[] prefixChars) { + if (index + prefixChars.length > pathChars.length) + return false; + + for (int i = 0; i < prefixChars.length; i++) { + char pathChar = pathChars[index + i]; + if (pathChar == '\\') + pathChar = '/'; + + char prefixChar = prefixChars[i]; + if (prefixChar == '\\') + prefixChar = '/'; + + if (pathChar != prefixChar) + return false; + } + + return true; + } }