Skip to content

Commit 80fea51

Browse files
committed
Handle Windows file separators in dockerignore patterns.
1 parent 4994811 commit 80fea51

File tree

5 files changed

+77
-29
lines changed

5 files changed

+77
-29
lines changed

client/src/main/groovy/de/gesellix/docker/client/builder/GlobsMatcher.groovy

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package de.gesellix.docker.client.builder
22

33
import groovy.util.logging.Slf4j
44

5+
import java.nio.file.FileSystem
56
import java.nio.file.FileSystems
67
import java.nio.file.Path
78
import java.nio.file.PathMatcher
@@ -10,32 +11,35 @@ import java.nio.file.PathMatcher
1011
class GlobsMatcher {
1112

1213
File base
14+
List<String> globs
1315
List<Matcher> matchers
1416

1517
GlobsMatcher(File base, List<String> globs) {
1618
this.base = base
17-
def fileSystem = FileSystems.getDefault()
18-
this.matchers = globs.collectMany {
19-
if (it.contains("\\")) {
20-
// if .dockerignore contains backslashes (for windows) something nasty will happen.
21-
// Caught: java.util.regex.PatternSyntaxException: No character to escape near index 3
22-
// bin\
23-
it = it.replaceAll('\\\\', '/')
24-
}
25-
if (it.endsWith("/")) {
26-
[new Matcher(fileSystem, it.replaceAll("/\$", "")),
27-
new Matcher(fileSystem, it.replaceAll("/\$", "/**"))]
28-
}
29-
else {
30-
[new Matcher(fileSystem, it)]
19+
this.globs = globs
20+
}
21+
22+
void initMatchers() {
23+
if (this.matchers == null) {
24+
def fileSystem = FileSystems.getDefault()
25+
this.matchers = globs.collectMany {
26+
if (it.endsWith("/")) {
27+
return [new Matcher(fileSystem, it.replaceAll("/\$", "")),
28+
new Matcher(fileSystem, it.replaceAll("/\$", "/**"))]
29+
}
30+
else {
31+
return [new Matcher(fileSystem, it)]
32+
}
33+
}.reverse()
34+
matchers.each {
35+
log.debug("pattern: ${it.pattern}")
3136
}
32-
}.reverse()
33-
matchers.each {
34-
log.debug("pattern: ${it.pattern}")
3537
}
3638
}
3739

38-
def matches(File path) {
40+
boolean matches(File path) {
41+
initMatchers()
42+
3943
def relativePath = base.absoluteFile.toPath().relativize(path.absoluteFile.toPath())
4044
def match = matchers.find {
4145
it.matches(relativePath)
@@ -54,18 +58,27 @@ class GlobsMatcher {
5458
PathMatcher matcher
5559
boolean negate
5660

57-
Matcher(fileSystem, String pattern) {
58-
this.negate = pattern.startsWith("!")
59-
this.pattern = pattern
61+
static String separator = File.separatorChar
62+
63+
Matcher(FileSystem fileSystem, String pattern) {
64+
this.pattern = pattern.replaceAll("/", "\\${separator}")
65+
.split("\\${separator}")
66+
.join("\\${separator}")
67+
String negation = "!"
68+
this.negate = pattern.startsWith(negation)
6069
if (this.negate) {
61-
def invertedPattern = pattern.substring("!".length())
62-
this.matcher = fileSystem.getPathMatcher("glob:${invertedPattern}")
70+
String invertedPattern = this.pattern.substring(negation.length())
71+
this.matcher = createGlob(fileSystem, invertedPattern)
6372
}
6473
else {
65-
this.matcher = fileSystem.getPathMatcher("glob:${pattern}")
74+
this.matcher = createGlob(fileSystem, this.pattern)
6675
}
6776
}
6877

78+
static PathMatcher createGlob(FileSystem fileSystem, String glob) {
79+
return fileSystem.getPathMatcher("glob:${glob}")
80+
}
81+
6982
@Override
7083
boolean matches(Path path) {
7184
return matcher.matches(path)

client/src/test/groovy/de/gesellix/docker/client/builder/DockerignoreFileFilterSpec.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package de.gesellix.docker.client.builder
22

33
import de.gesellix.util.IOUtils
4+
import org.apache.commons.lang.SystemUtils
5+
import spock.lang.Requires
46
import spock.lang.Specification
57

68
class DockerignoreFileFilterSpec extends Specification {
@@ -39,8 +41,8 @@ class DockerignoreFileFilterSpec extends Specification {
3941
new File("${baseDir}/keepme/subdir/keep-me.txt")].sort()
4042
}
4143

42-
43-
def "handles backslashes in patterns (for windows)"() {
44+
@Requires({ SystemUtils.IS_OS_WINDOWS })
45+
def "handles trailing backslashes in patterns (windows only)"() {
4446
given:
4547
def baseDir = IOUtils.getResource("/dockerignore_windows").file
4648
def base = new File(baseDir)

client/src/test/groovy/de/gesellix/docker/client/builder/GlobsMatcherSpec.groovy

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package de.gesellix.docker.client.builder
22

33
import org.apache.commons.lang.SystemUtils
4-
import spock.lang.IgnoreIf
4+
import spock.lang.Requires
55
import spock.lang.Specification
66
import spock.lang.Unroll
77

@@ -12,6 +12,7 @@ class GlobsMatcherSpec extends Specification {
1212
def "matches all patterns"() {
1313
given:
1414
def matcher = new GlobsMatcher(new File(""), ["abc", "cde"])
15+
matcher.initMatchers()
1516

1617
expect:
1718
matcher.matchers.size() == 2
@@ -48,7 +49,7 @@ class GlobsMatcherSpec extends Specification {
4849
"[-]" | new File("") | new File("-")
4950
}
5051

51-
@IgnoreIf({ !SystemUtils.IS_OS_LINUX && !SystemUtils.IS_OS_MAC })
52+
@Requires({ SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC })
5253
@Unroll
5354
"#pattern should match #path on unix systems"(String pattern, File base, File path) {
5455
expect:
@@ -63,6 +64,17 @@ class GlobsMatcherSpec extends Specification {
6364
// "[\\]a]" | new File("") | new File("]")
6465
}
6566

67+
@Requires({ SystemUtils.IS_OS_WINDOWS })
68+
@Unroll
69+
"#pattern should match #path on windows systems"(String pattern, File base, File path) {
70+
expect:
71+
new GlobsMatcher(base, [pattern]).matches(path)
72+
73+
where:
74+
pattern | base | path
75+
"bin\\" | new File("") | new File("bin\\foo")
76+
}
77+
6678
@Unroll
6779
"#pattern should not match #path"(String pattern, File path) {
6880
expect:
@@ -83,7 +95,7 @@ class GlobsMatcherSpec extends Specification {
8395
"a*b" | new File("a/b")
8496
}
8597

86-
@IgnoreIf({ !SystemUtils.IS_OS_LINUX && !SystemUtils.IS_OS_MAC })
98+
@Requires({ SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC })
8799
@Unroll
88100
"#pattern should not match #path on unix systems"(String pattern, File base, File path) {
89101
expect:
@@ -109,6 +121,23 @@ class GlobsMatcherSpec extends Specification {
109121
"[]a]" | new File("") | new File("]")
110122
}
111123

124+
@Requires({ SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC })
125+
@Unroll
126+
"#pattern should throw exception on unix systems"(String pattern, File base, File path) {
127+
when:
128+
new GlobsMatcher(base, [pattern]).matches(path)
129+
130+
then:
131+
thrown(PatternSyntaxException)
132+
133+
where:
134+
pattern | base | path
135+
// On *nix, the backslash is the escape character, so it's invalid to be used at the end of a pattern
136+
// A trailing backslash is only valid on Windows.
137+
// We actually differ from the official client's behaviour, which silently ignores invalid patterns.
138+
"bin\\" | new File("") | new File("bin/foo")
139+
}
140+
112141
def "allows pattern exclusions"() {
113142
expect:
114143
new GlobsMatcher(new File(""), patterns).matches(path) == shouldMatch
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
bin\
2+
Dockerfile
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM microsoft/aspnetcore:2.0
2+
COPY . /example
3+
RUN ls -lisah /example

0 commit comments

Comments
 (0)