From 960f898dff30fb0c819a39bde78846ccd72d2be5 Mon Sep 17 00:00:00 2001 From: Damien Churchill Date: Tue, 11 Jun 2019 16:56:09 +0100 Subject: [PATCH] drivers/docker: move lifted code out to separate file and link the source & license --- drivers/docker/utils.go | 140 ------------------------ drivers/docker/win32_volume_parse.go | 152 +++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 140 deletions(-) create mode 100644 drivers/docker/win32_volume_parse.go diff --git a/drivers/docker/utils.go b/drivers/docker/utils.go index c1dc4a83f..20e03608b 100644 --- a/drivers/docker/utils.go +++ b/drivers/docker/utils.go @@ -6,74 +6,12 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strings" "github.com/docker/cli/cli/config/configfile" "github.com/docker/distribution/reference" "github.com/docker/docker/registry" docker "github.com/fsouza/go-dockerclient" - "github.com/pkg/errors" -) - -const ( - // Spec should be in the format [source:]destination[:mode] - // - // Examples: c:\foo bar:d:rw - // c:\foo:d:\bar - // myname:d: - // d:\ - // - // Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See - // https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to - // test is https://regex-golang.appspot.com/assets/html/index.html - // - // Useful link for referencing named capturing groups: - // http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex - // - // There are three match groups: source, destination and mode. - // - - // rxHostDir is the first option of a source - rxHostDir = `(?:\\\\\?\\)?[a-z]:[\\/](?:[^\\/:*?"<>|\r\n]+[\\/]?)*` - // rxName is the second option of a source - rxName = `[^\\/:*?"<>|\r\n]+` - - // RXReservedNames are reserved names not possible on Windows - rxReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])` - - // rxPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \) - rxPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+` - // rxSource is the combined possibilities for a source - rxSource = `((?P((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?` - - // 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 - rxDestination = `(?P((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))` - - // 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 - rxMode = `(:(?P(?i)ro|rw))?` ) func parseDockerImage(image string) (repo, tag string) { @@ -282,10 +220,6 @@ func isParentPath(parent, path string) bool { return err == nil && !strings.HasPrefix(rel, "..") } -func errInvalidSpec(spec string) error { - return errors.Errorf("invalid volume specification: '%s'", spec) -} - func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, mode string, err error) { if os == "windows" { return parseVolumeSpecWindows(volBind) @@ -293,80 +227,6 @@ func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, return parseVolumeSpecLinux(volBind) } -type fileInfoProvider interface { - fileInfo(path string) (exist, isDir bool, err error) -} - -type defaultFileInfoProvider struct { -} - -func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) { - fi, err := os.Stat(path) - if err != nil { - if !os.IsNotExist(err) { - return false, false, err - } - return false, false, nil - } - return true, fi.IsDir(), nil -} - -var currentFileInfoProvider fileInfoProvider = defaultFileInfoProvider{} - -func windowsSplitRawSpec(raw, destRegex string) ([]string, error) { - specExp := regexp.MustCompile(`^` + rxSource + destRegex + rxMode + `$`) - match := specExp.FindStringSubmatch(strings.ToLower(raw)) - - // Must have something back - if len(match) == 0 { - return nil, errInvalidSpec(raw) - } - - var split []string - matchgroups := make(map[string]string) - // Pull out the sub expressions from the named capture groups - for i, name := range specExp.SubexpNames() { - matchgroups[name] = strings.ToLower(match[i]) - } - if source, exists := matchgroups["source"]; exists { - if source != "" { - split = append(split, source) - } - } - if destination, exists := matchgroups["destination"]; exists { - if destination != "" { - split = append(split, destination) - } - } - if mode, exists := matchgroups["mode"]; exists { - if mode != "" { - split = append(split, mode) - } - } - // Fix #26329. If the destination appears to be a file, and the source is null, - // it may be because we've fallen through the possible naming regex and hit a - // situation where the user intention was to map a file into a container through - // a local volume, but this is not supported by the platform. - if matchgroups["source"] == "" && matchgroups["destination"] != "" { - volExp := regexp.MustCompile(`^` + rxName + `$`) - reservedNameExp := regexp.MustCompile(`^` + rxReservedNames + `$`) - - if volExp.MatchString(matchgroups["destination"]) { - if reservedNameExp.MatchString(matchgroups["destination"]) { - return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"]) - } - } else { - - exists, isDir, _ := currentFileInfoProvider.fileInfo(matchgroups["destination"]) - if exists && !isDir { - return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"]) - - } - } - } - return split, nil -} - func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) { parts, err := windowsSplitRawSpec(volBind, rxDestination) if err != nil { diff --git a/drivers/docker/win32_volume_parse.go b/drivers/docker/win32_volume_parse.go new file mode 100644 index 000000000..132747bee --- /dev/null +++ b/drivers/docker/win32_volume_parse.go @@ -0,0 +1,152 @@ +package docker + +import ( + "fmt" + "regexp" + "os" + "strings" + "github.com/pkg/errors" +) + +// This code is taken from github.com/docker/volume/mounts/windows_parser.go +// See https://github.com/moby/moby/blob/master/LICENSE for the license, Apache License 2.0 at this time. + +const ( + // Spec should be in the format [source:]destination[:mode] + // + // Examples: c:\foo bar:d:rw + // c:\foo:d:\bar + // myname:d: + // d:\ + // + // Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See + // https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to + // test is https://regex-golang.appspot.com/assets/html/index.html + // + // Useful link for referencing named capturing groups: + // http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex + // + // There are three match groups: source, destination and mode. + // + + // rxHostDir is the first option of a source + rxHostDir = `(?:\\\\\?\\)?[a-z]:[\\/](?:[^\\/:*?"<>|\r\n]+[\\/]?)*` + // rxName is the second option of a source + rxName = `[^\\/:*?"<>|\r\n]+` + + // RXReservedNames are reserved names not possible on Windows + rxReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])` + + // rxPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \) + rxPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+` + // rxSource is the combined possibilities for a source + rxSource = `((?P((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?` + + // 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 + rxDestination = `(?P((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))` + + // 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 + rxMode = `(:(?P(?i)ro|rw))?` +) + + +func errInvalidSpec(spec string) error { + return errors.Errorf("invalid volume specification: '%s'", spec) +} + +type fileInfoProvider interface { + fileInfo(path string) (exist, isDir bool, err error) +} + +type defaultFileInfoProvider struct { +} + +func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) { + fi, err := os.Stat(path) + if err != nil { + if !os.IsNotExist(err) { + return false, false, err + } + return false, false, nil + } + return true, fi.IsDir(), nil +} + +var currentFileInfoProvider fileInfoProvider = defaultFileInfoProvider{} + +func windowsSplitRawSpec(raw, destRegex string) ([]string, error) { + specExp := regexp.MustCompile(`^` + rxSource + destRegex + rxMode + `$`) + match := specExp.FindStringSubmatch(strings.ToLower(raw)) + + // Must have something back + if len(match) == 0 { + return nil, errInvalidSpec(raw) + } + + var split []string + matchgroups := make(map[string]string) + // Pull out the sub expressions from the named capture groups + for i, name := range specExp.SubexpNames() { + matchgroups[name] = strings.ToLower(match[i]) + } + if source, exists := matchgroups["source"]; exists { + if source != "" { + split = append(split, source) + } + } + if destination, exists := matchgroups["destination"]; exists { + if destination != "" { + split = append(split, destination) + } + } + if mode, exists := matchgroups["mode"]; exists { + if mode != "" { + split = append(split, mode) + } + } + // Fix #26329. If the destination appears to be a file, and the source is null, + // it may be because we've fallen through the possible naming regex and hit a + // situation where the user intention was to map a file into a container through + // a local volume, but this is not supported by the platform. + if matchgroups["source"] == "" && matchgroups["destination"] != "" { + volExp := regexp.MustCompile(`^` + rxName + `$`) + reservedNameExp := regexp.MustCompile(`^` + rxReservedNames + `$`) + + if volExp.MatchString(matchgroups["destination"]) { + if reservedNameExp.MatchString(matchgroups["destination"]) { + return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"]) + } + } else { + + exists, isDir, _ := currentFileInfoProvider.fileInfo(matchgroups["destination"]) + if exists && !isDir { + return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"]) + + } + } + } + return split, nil +} +