package volume import ( "fmt" "os" "path/filepath" "regexp" "strings" "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/system" ) // read-write modes var rwModes = map[string]bool{ "rw": true, } // read-only modes var roModes = map[string]bool{ "ro": true, } 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])` // RXSource is the combined possibilities for a source RXSource = `((?P((` + RXHostDir + `)|(` + RXName + `))):)?` // 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]+)*\\?))` // 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 var RXMode string func init() { osv := system.GetOSVersion() // Read-only volumes supported from 14350 onwards (post Windows Server 2016 TP5) // Mode (optional): // - Hopefully self explanatory in comparison to above regex's. // - Colon is not in the capture group if osv.Build >= 14350 { RXMode = `(:(?P(?i)ro|rw))?` } else { RXMode = `(:(?P(?i)rw))?` } } // BackwardsCompatible decides whether this mount point can be // used in old versions of Docker or not. // Windows volumes are never backwards compatible. func (m *MountPoint) BackwardsCompatible() bool { return false } // ParseMountSpec validates the configuration of mount information is valid. func ParseMountSpec(spec string, volumeDriver string) (*MountPoint, error) { var specExp = regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`) // Ensure in platform semantics for matching. The CLI will send in Unix semantics. match := specExp.FindStringSubmatch(filepath.FromSlash(strings.ToLower(spec))) // Must have something back if len(match) == 0 { return nil, errInvalidSpec(spec) } // Pull out the sub expressions from the named capture groups matchgroups := make(map[string]string) for i, name := range specExp.SubexpNames() { matchgroups[name] = strings.ToLower(match[i]) } mp := &MountPoint{ Source: matchgroups["source"], Destination: matchgroups["destination"], RW: true, } if strings.ToLower(matchgroups["mode"]) == "ro" { mp.RW = false } // Volumes cannot include an explicitly supplied mode eg c:\path:rw if mp.Source == "" && mp.Destination != "" && matchgroups["mode"] != "" { return nil, errInvalidSpec(spec) } // Note: No need to check if destination is absolute as it must be by // definition of matching the regex. if filepath.VolumeName(mp.Destination) == mp.Destination { // Ensure the destination path, if a drive letter, is not the c drive if strings.ToLower(mp.Destination) == "c:" { return nil, fmt.Errorf("Destination drive letter in '%s' cannot be c:", spec) } } else { // So we know the destination is a path, not drive letter. Clean it up. mp.Destination = filepath.Clean(mp.Destination) // Ensure the destination path, if a path, is not the c root directory if strings.ToLower(mp.Destination) == `c:\` { return nil, fmt.Errorf(`Destination path in '%s' cannot be c:\`, spec) } } // See if the source is a name instead of a host directory if len(mp.Source) > 0 { validName, err := IsVolumeNameValid(mp.Source) if err != nil { return nil, err } if validName { // OK, so the source is a name. mp.Name = mp.Source mp.Source = "" // Set the driver accordingly mp.Driver = volumeDriver if len(mp.Driver) == 0 { mp.Driver = DefaultDriverName } } else { // OK, so the source must be a host directory. Make sure it's clean. mp.Source = filepath.Clean(mp.Source) } } // Ensure the host path source, if supplied, exists and is a directory if len(mp.Source) > 0 { var fi os.FileInfo var err error if fi, err = os.Stat(mp.Source); err != nil { return nil, fmt.Errorf("Source directory '%s' could not be found: %s", mp.Source, err) } if !fi.IsDir() { return nil, fmt.Errorf("Source '%s' is not a directory", mp.Source) } } // 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 len(mp.Source) == 0 && len(mp.Destination) > 0 { var fi os.FileInfo var err error if fi, err = os.Stat(mp.Destination); err == nil { validName, err := IsVolumeNameValid(mp.Destination) if err != nil { return nil, err } if !validName && !fi.IsDir() { return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", mp.Destination) } } } logrus.Debugf("MP: Source '%s', Dest '%s', RW %t, Name '%s', Driver '%s'", mp.Source, mp.Destination, mp.RW, mp.Name, mp.Driver) return mp, nil } // IsVolumeNameValid checks a volume name in a platform specific manner. func IsVolumeNameValid(name string) (bool, error) { nameExp := regexp.MustCompile(`^` + RXName + `$`) if !nameExp.MatchString(name) { return false, nil } nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`) if nameExp.MatchString(name) { return false, fmt.Errorf("Volume name %q cannot be a reserved word for Windows filenames", name) } return true, nil } // ValidMountMode will make sure the mount mode is valid. // returns if it's a valid mount mode or not. func ValidMountMode(mode string) bool { return roModes[strings.ToLower(mode)] || rwModes[strings.ToLower(mode)] } // ReadWrite tells you if a mode string is a valid read-write mode or not. func ReadWrite(mode string) bool { return rwModes[strings.ToLower(mode)] }