diff --git a/src/main/java/com/github/dockerjava/api/model/Device.java b/src/main/java/com/github/dockerjava/api/model/Device.java index 9240239b1..f2b75e3f5 100644 --- a/src/main/java/com/github/dockerjava/api/model/Device.java +++ b/src/main/java/com/github/dockerjava/api/model/Device.java @@ -1,6 +1,8 @@ package com.github.dockerjava.api.model; import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.commons.lang.BooleanUtils.isNotTrue; +import static org.apache.commons.lang.StringUtils.isEmpty; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; @@ -9,6 +11,11 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + @JsonInclude(Include.NON_NULL) public class Device { @@ -45,6 +52,75 @@ public String getPathOnHost() { return pathOnHost; } + /** + * @link https://github.com/docker/docker/blob/6b4a46f28266031ce1a1315f17fb69113a06efe1/runconfig/opts/parse_test.go#L468 + */ + @Nonnull + public static Device parse(@Nonnull String deviceStr) { + String src = ""; + String dst = ""; + String permissions = "rwm"; + final String[] arr = deviceStr.trim().split(":"); + // java String.split() returns wrong length, use tokenizer instead + switch (new StringTokenizer(deviceStr, ":").countTokens()) { + case 3: { + // Mismatches docker code logic. While there is no validations after parsing, checking heregit + if (validDeviceMode(arr[2])) { + permissions = arr[2]; + } else { + throw new IllegalArgumentException("Invalid device specification: " + deviceStr); + } + } + case 2: { + if (validDeviceMode(arr[1])) { + permissions = arr[1]; + } else { + dst = arr[1]; + } + } + case 1: { + src = arr[0]; + break; + } + default: { + throw new IllegalArgumentException("Invalid device specification: " + deviceStr); + } + } + + if (isEmpty(dst)) { + dst = src; + } + + return new Device(permissions, dst, src); + } + + /** + * ValidDeviceMode checks if the mode for device is valid or not. + * Valid mode is a composition of r (read), w (write), and m (mknod). + * + * @link https://github.com/docker/docker/blob/6b4a46f28266031ce1a1315f17fb69113a06efe1/runconfig/opts/parse.go#L796 + */ + private static boolean validDeviceMode(String deviceMode) { + Map validModes = new HashMap<>(3); + validModes.put("r", true); + validModes.put("w", true); + validModes.put("m", true); + + if (isEmpty(deviceMode)) { + return false; + } + + for (char ch : deviceMode.toCharArray()) { + final String mode = String.valueOf(ch); + if (isNotTrue(validModes.get(mode))) { + return false; // wrong mode + } + validModes.put(mode, false); + } + + return true; + } + @Override public boolean equals(Object obj) { if (obj instanceof Device) { diff --git a/src/test/java/com/github/dockerjava/api/model/DeviceTest.java b/src/test/java/com/github/dockerjava/api/model/DeviceTest.java new file mode 100644 index 000000000..5c48c4886 --- /dev/null +++ b/src/test/java/com/github/dockerjava/api/model/DeviceTest.java @@ -0,0 +1,95 @@ +package com.github.dockerjava.api.model; + +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static junit.framework.Assert.fail; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +/** + * @author Kanstantsin Shautsou + */ +public class DeviceTest { + + public static List validPaths = Arrays.asList( + "/home", + "/home:/home", + "/home:/something/else", + "/with space", + "/home:/with space", + "relative:/absolute-path", + "hostPath:/containerPath:r", + "/hostPath:/containerPath:rw", + "/hostPath:/containerPath:mrw" + ); + + public static HashMap badPaths = new LinkedHashMap() {{ + put("", "bad format for path: "); + // TODO implement ValidatePath +// put("./", "./ is not an absolute path"); +// put("../", "../ is not an absolute path"); +// put("/:../", "../ is not an absolute path"); +// put("/:path", "path is not an absolute path"); +// put(":", "bad format for path: :"); +// put("/tmp:", " is not an absolute path"); +// put(":test", "bad format for path: :test"); +// put(":/test", "bad format for path: :/test"); +// put("tmp:", " is not an absolute path"); +// put(":test:", "bad format for path: :test:"); +// put("::", "bad format for path: ::"); +// put(":::", "bad format for path: :::"); +// put("/tmp:::", "bad format for path: /tmp:::"); +// put(":/tmp::", "bad format for path: :/tmp::"); +// put("path:ro", "ro is not an absolute path"); +// put("path:rr", "rr is not an absolute path"); + put("a:/b:ro", "bad mode specified: ro"); + put("a:/b:rr", "bad mode specified: rr"); + }}; + + @Test + public void testParse() throws Exception { + assertThat(Device.parse("/dev/sda:/dev/xvdc:r"), + equalTo(new Device("r", "/dev/xvdc", "/dev/sda"))); + + assertThat(Device.parse("/dev/snd:rw"), + equalTo(new Device("rw", "/dev/snd", "/dev/snd"))); + + assertThat(Device.parse("/dev/snd:/something"), + equalTo(new Device("rwm", "/something", "/dev/snd"))); + + assertThat(Device.parse("/dev/snd:/something:rw"), + equalTo(new Device("rw", "/something", "/dev/snd"))); + + } + + @Test + public void testParseBadPaths() { + for (Map.Entry entry : badPaths.entrySet()) { + final String deviceStr = entry.getKey(); + try { + Device.parse(deviceStr); + fail("Should fail because: " + entry.getValue() + " '" + deviceStr + "'"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage(), containsString("Invalid device specification:")); + } + } + } + + @Test + public void testParseValidPaths() { + for (String path : validPaths) { + Device.parse(path); + } + } +}