diff --git a/src/main/java/com/github/dockerjava/api/model/Bind.java b/src/main/java/com/github/dockerjava/api/model/Bind.java index 9a7ebf18d..05e88ea28 100644 --- a/src/main/java/com/github/dockerjava/api/model/Bind.java +++ b/src/main/java/com/github/dockerjava/api/model/Bind.java @@ -86,6 +86,19 @@ public PropagationMode getPropagationMode() { return propagationMode; } + /** + * Parses a bind mount specification to a {@link Bind} using the {@link BinwWindows} parser + * @param serialized + * the specification, e.g. c:\host:c:\container:ro|\\r\\n]+\\\\?)*"; + + // RXName is the second option of a source + private static String rxName = "[^\\\\/:*?\"<>|\\r\\n]+"; + + // RXReservedNames are reserved names not possible on Windows + // private static String rxReservedNames = "(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])"; + + // RXSource is the combined possibilities for a source + private static String rxSource = "((?((" + rxHostDir + ")|(" + rxName + "))):)?"; + + // Source. Can be either a host directory, a name, or omitted: + // HostDir: + // - Essentially using the folder solution from + // https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html + // but adding case insensitivity. + // - Must be an absolute path such as c:\path + // - Can include spaces such as `c:\program files` + // - And then followed by a colon which is not in the capture group + // - And can be optional + // Name: + // - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx) + // - And then followed by a colon which is not in the capture group + // - And can be optional + + // RXDestination is the regex expression for the mount destination + private static String rxDestination = "(?([a-z]):((?:\\\\[^\\\\/:*?\"<>\\r\\n]+)*\\\\?))"; + // Destination (aka container path): + // - Variation on hostdir but can be a drive followed by colon as well + // - If a path, must be absolute. Can include spaces + // - Drive cannot be c: (explicitly checked in code, not RegEx) + + // RXMode is the regex expression for the mode of the mount + // Mode (optional): + // - Hopefully self explanatory in comparison to above regex's. + // - Colon is not in the capture group + private static String rxMode = "(:(?(?i)ro|rw))?"; + + private static Pattern pattern; + + BindWindows(String source, String destination, String[] flags) { + this.source = source; + this.destination = destination; + this.flags = flags; + } + + public String[] getFlags() { + return flags; + } + + public String getSource() { + return source; + } + + public String getDestination() { + return destination; + } + + public static BindWindows splitRawSpec(String raw) throws IllegalArgumentException { + + if (pattern == null) { + pattern = Pattern.compile("^" + rxSource + rxDestination + rxMode + "$"); + } + + Matcher matcher = pattern.matcher(raw.toLowerCase()); + + if (!matcher.find()) { + throw new IllegalArgumentException(); + } + + String source = matcher.group("source"); + String destination = matcher.group("destination"); + String mode = matcher.group("mode"); + + return new BindWindows(source, destination, (mode == null ? "" : mode).split(",")); + } + + + /** + * Returns a string representation of this {@link Bind} suitable for inclusion in a JSON message. + * The format is <host path>:<container path>:<access mode>, + * like the argument in {@link #parse(String)}. + * + * @return a string representation of this {@link Bind} + */ + @Override + public String toString() { + return String.format("%s:%s%s", + source, + destination, + String.join(",", flags).length() > 0 ? ":" + String.join(",", flags) : ""); + } +} diff --git a/src/test/java/com/github/dockerjava/api/model/BindWindowsTest.java b/src/test/java/com/github/dockerjava/api/model/BindWindowsTest.java new file mode 100644 index 000000000..07394bc83 --- /dev/null +++ b/src/test/java/com/github/dockerjava/api/model/BindWindowsTest.java @@ -0,0 +1,75 @@ +package com.github.dockerjava.api.model; + +import static com.github.dockerjava.api.model.AccessMode.ro; +import static com.github.dockerjava.api.model.AccessMode.rw; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; + +import org.testng.annotations.Test; + +public class BindWindowsTest { + + @Test + public void parseUsingDefaultAccessMode() { + Bind bind = Bind.parse("c:\\host:c:\\container"); + assertThat(bind.getPath(), is("c:\\host")); + assertThat(bind.getVolume().getPath(), is("c:\\container")); + assertThat(bind.getAccessMode(), is(AccessMode.DEFAULT)); + assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); + } + + @Test + public void parseReadWrite() { + Bind bind = Bind.parse("c:\\host:c:\\container:rw"); + assertThat(bind.getPath(), is("c:\\host")); + assertThat(bind.getVolume().getPath(), is("c:\\container")); + assertThat(bind.getAccessMode(), is(rw)); + assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); + } + + @Test + public void parseReadOnly() { + Bind bind = Bind.parse("c:\\host:c:\\container:ro"); + assertThat(bind.getPath(), is("c:\\host")); + assertThat(bind.getVolume().getPath(), is("c:\\container")); + assertThat(bind.getAccessMode(), is(ro)); + assertThat(bind.getSecMode(), is(SELContext.none)); + assertThat(bind.getNoCopy(), nullValue()); + assertThat(bind.getPropagationMode(), is(PropagationMode.DEFAULT_MODE)); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Error parsing Bind.*") + public void parseInvalidAccessMode() { + Bind.parse("c:\\host:c:\\container:xx"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Error parsing Bind 'nonsense'") + public void parseInvalidInput() { + Bind.parse("nonsense"); + } + + @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "Error parsing Bind 'null'") + public void parseNull() { + Bind.parse(null); + } + + @Test + public void toStringReadOnly() { + assertThat(Bind.parse("c:\\host:c:\\container:ro").toString(), is("c:\\host:c:\\container:ro")); + } + + @Test + public void toStringReadWrite() { + assertThat(Bind.parse("c:\\host:c:\\container:rw").toString(), is("c:\\host:c:\\container:rw")); + } + + @Test + public void toStringDefaultAccessMode() { + assertThat(Bind.parse("c:\\host:c:\\container").toString(), is("c:\\host:c:\\container:rw")); + } +}