Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/main/java/com/github/dockerjava/api/model/Device.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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<String, Boolean> validModes = new HashMap<>(3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The `validModes' map should be declared as static member and not declared inline.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean under not declared inline.?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean it should be declared at the class level not in the method itself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It must be mutable, it changes true to false to exclude double letters.

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if char repeats -> wrong mode

}
validModes.put(mode, false);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it for? Some kind of optimization?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error duplicated modes i.e. rrr

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Now I got it. So it must be declared within the method then. Sorry for bothering.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it tricky logic :)

}

return true;
}

@Override
public boolean equals(Object obj) {
if (obj instanceof Device) {
Expand Down
95 changes: 95 additions & 0 deletions src/test/java/com/github/dockerjava/api/model/DeviceTest.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String, String> badPaths = new LinkedHashMap<String, String>() {{
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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is missing the invalid mode case.

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<String, String> 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);
}
}
}