Repository: docker/docker-py

Issue:
Regression in .dockerignore parsing with trailing '**'
docker==2.5.0
docker-compose==1.15.0
docker-pycreds==0.2.1
dockerpty==0.4.1

Having a line with trailing '**' in .dockerignore is causing an IndexError when calling docker-compose. Version 2.4.2 does not cause a crash. E.g. the following line causes the crash:

subdir/**


Traceback (most recent call last):
  File "/usr/local/bin/docker-compose", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python2.7/dist-packages/compose/cli/main.py", line 68, in main
    command()
  File "/usr/local/lib/python2.7/dist-packages/compose/cli/main.py", line 118, in perform_command
    handler(command, command_options)
  File "/usr/local/lib/python2.7/dist-packages/compose/cli/main.py", line 239, in build
    build_args=build_args)
  File "/usr/local/lib/python2.7/dist-packages/compose/project.py", line 340, in build
    service.build(no_cache, pull, force_rm, build_args)
  File "/usr/local/lib/python2.7/dist-packages/compose/service.py", line 910, in build
    network_mode=build_opts.get('network', None),
  File "/usr/local/lib/python2.7/dist-packages/docker/api/build.py", line 148, in build
    path, exclude=exclude, dockerfile=dockerfile, gzip=gzip
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/build.py", line 13, in tar
    files=sorted(exclude_paths(root, exclude, dockerfile=dockerfile)),
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/build.py", line 38, in exclude_paths
    has_exceptions=len(exceptions) > 0)
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/build.py", line 113, in get_paths
    os.path.join(parent, d), exclude_patterns, include_patterns
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/build.py", line 94, in should_check_directory
    directory_path, exclude_patterns, include_patterns
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/build.py", line 60, in should_include
    if match_path(path, pattern):
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/build.py", line 143, in match_path
    return fnmatch('/'.join(path_components), '/'.join(pattern_components))
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/fnmatch.py", line 44, in fnmatch
    return fnmatchcase(name, pat)
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/fnmatch.py", line 56, in fnmatchcase
    res = translate(pat)
  File "/usr/local/lib/python2.7/dist-packages/docker/utils/fnmatch.py", line 78, in translate
    if pat[i] == '/':
IndexError: string index out of range

The check for the trailing slash in fnmatch.py (https://github.com/docker/docker-py/blob/d798afca7e866641a357275ae768a0a686d68882/docker/utils/fnmatch.py#L78)
seems to be missing boundary checks after the index has been incremented
on previous line.


Discussion:

Resolving patch:
diff --git a/docker/utils/fnmatch.py b/docker/utils/fnmatch.py
--- a/docker/utils/fnmatch.py
+++ b/docker/utils/fnmatch.py
@@ -75,7 +75,7 @@ def translate(pat):
                 # is some flavor of "**"
                 i = i + 1
                 # Treat **/ as ** so eat the "/"
-                if pat[i] == '/':
+                if i < n and pat[i] == '/':
                     i = i + 1
                 if i >= n:
                     # is "**EOF" - to align with .gitignore just accept all


Test patch:
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
--- a/tests/unit/utils_test.py
+++ b/tests/unit/utils_test.py
@@ -874,6 +874,23 @@ def test_single_and_double_wildcard(self):
             )
         )
 
+    def test_trailing_double_wildcard(self):
+        assert self.exclude(['subdir/**']) == convert_paths(
+            self.all_paths - set(
+                ['subdir/file.txt',
+                 'subdir/target/file.txt',
+                 'subdir/target/subdir/file.txt',
+                 'subdir/subdir2/file.txt',
+                 'subdir/subdir2/target/file.txt',
+                 'subdir/subdir2/target/subdir/file.txt',
+                 'subdir/target',
+                 'subdir/target/subdir',
+                 'subdir/subdir2',
+                 'subdir/subdir2/target',
+                 'subdir/subdir2/target/subdir']
+            )
+        )
+
 
 class TarTest(unittest.TestCase):
     def test_tar_with_excludes(self):