package volume // import "github.com/docker/docker/volume" import ( "errors" "fmt" "os" "regexp" "runtime" "strings" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/pkg/stringid" ) type windowsParser struct { } 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 + `))` rxLCOWDestination = `(?P/(?:[^\\/:*?"<>\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 // Mode (optional): // - Hopefully self explanatory in comparison to above regex's. // - Colon is not in the capture group rxMode = `(:(?P(?i)ro|rw))?` ) type mountValidator func(mnt *mount.Mount) error 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 windowsValidMountMode(mode string) bool { if mode == "" { return true } return rwModes[strings.ToLower(mode)] } func windowsValidateNotRoot(p string) error { p = strings.ToLower(strings.Replace(p, `/`, `\`, -1)) if p == "c:" || p == `c:\` { return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p) } return nil } var windowsSpecificValidators mountValidator = func(mnt *mount.Mount) error { return windowsValidateNotRoot(mnt.Target) } func windowsValidateRegex(p, r string) error { if regexp.MustCompile(`^` + r + `$`).MatchString(strings.ToLower(p)) { return nil } return fmt.Errorf("invalid mount path: '%s'", p) } func windowsValidateAbsolute(p string) error { if err := windowsValidateRegex(p, rxDestination); err != nil { return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p) } return nil } func windowsDetectMountType(p string) mount.Type { if strings.HasPrefix(p, `\\.\pipe\`) { return mount.TypeNamedPipe } else if regexp.MustCompile(`^` + rxHostDir + `$`).MatchString(p) { return mount.TypeBind } else { return mount.TypeVolume } } func (p *windowsParser) ReadWrite(mode string) bool { return strings.ToLower(mode) != "ro" } // IsVolumeNameValid checks a volume name in a platform specific manner. func (p *windowsParser) ValidateVolumeName(name string) error { nameExp := regexp.MustCompile(`^` + rxName + `$`) if !nameExp.MatchString(name) { return errors.New("invalid volume name") } nameExp = regexp.MustCompile(`^` + rxReservedNames + `$`) if nameExp.MatchString(name) { return fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name) } return nil } func (p *windowsParser) ValidateMountConfig(mnt *mount.Mount) error { return p.validateMountConfigReg(mnt, rxDestination, windowsSpecificValidators) } 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 (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, destRegex string, additionalValidators ...mountValidator) error { for _, v := range additionalValidators { if err := v(mnt); err != nil { return &errMountConfig{mnt, err} } } if len(mnt.Target) == 0 { return &errMountConfig{mnt, errMissingField("Target")} } if err := windowsValidateRegex(mnt.Target, destRegex); err != nil { return &errMountConfig{mnt, err} } switch mnt.Type { case mount.TypeBind: if len(mnt.Source) == 0 { return &errMountConfig{mnt, errMissingField("Source")} } // Don't error out just because the propagation mode is not supported on the platform if opts := mnt.BindOptions; opts != nil { if len(opts.Propagation) > 0 { return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)} } } if mnt.VolumeOptions != nil { return &errMountConfig{mnt, errExtraField("VolumeOptions")} } if err := windowsValidateAbsolute(mnt.Source); err != nil { return &errMountConfig{mnt, err} } exists, isdir, err := currentFileInfoProvider.fileInfo(mnt.Source) if err != nil { return &errMountConfig{mnt, err} } if !exists { return &errMountConfig{mnt, errBindNotExist} } if !isdir { return &errMountConfig{mnt, fmt.Errorf("source path must be a directory")} } case mount.TypeVolume: if mnt.BindOptions != nil { return &errMountConfig{mnt, errExtraField("BindOptions")} } if len(mnt.Source) == 0 && mnt.ReadOnly { return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")} } if len(mnt.Source) != 0 { if err := p.ValidateVolumeName(mnt.Source); err != nil { return &errMountConfig{mnt, err} } } case mount.TypeNamedPipe: if len(mnt.Source) == 0 { return &errMountConfig{mnt, errMissingField("Source")} } if mnt.BindOptions != nil { return &errMountConfig{mnt, errExtraField("BindOptions")} } if mnt.ReadOnly { return &errMountConfig{mnt, errExtraField("ReadOnly")} } if windowsDetectMountType(mnt.Source) != mount.TypeNamedPipe { return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)} } if windowsDetectMountType(mnt.Target) != mount.TypeNamedPipe { return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)} } default: return &errMountConfig{mnt, errors.New("mount type unknown")} } return nil } func (p *windowsParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) { return p.parseMountRaw(raw, volumeDriver, rxDestination, true, windowsSpecificValidators) } func (p *windowsParser) parseMountRaw(raw, volumeDriver, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) { arr, err := windowsSplitRawSpec(raw, destRegex) if err != nil { return nil, err } var spec mount.Mount var mode string switch len(arr) { case 1: // Just a destination path in the container spec.Target = arr[0] case 2: if windowsValidMountMode(arr[1]) { // Destination + Mode is not a valid volume - volumes // cannot include a mode. e.g. /foo:rw return nil, errInvalidSpec(raw) } // Host Source Path or Name + Destination spec.Source = strings.Replace(arr[0], `/`, `\`, -1) spec.Target = arr[1] case 3: // HostSourcePath+DestinationPath+Mode spec.Source = strings.Replace(arr[0], `/`, `\`, -1) spec.Target = arr[1] mode = arr[2] default: return nil, errInvalidSpec(raw) } if convertTargetToBackslash { spec.Target = strings.Replace(spec.Target, `/`, `\`, -1) } if !windowsValidMountMode(mode) { return nil, errInvalidMode(mode) } spec.Type = windowsDetectMountType(spec.Source) spec.ReadOnly = !p.ReadWrite(mode) // cannot assume that if a volume driver is passed in that we should set it if volumeDriver != "" && spec.Type == mount.TypeVolume { spec.VolumeOptions = &mount.VolumeOptions{ DriverConfig: &mount.Driver{Name: volumeDriver}, } } if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet { if spec.VolumeOptions == nil { spec.VolumeOptions = &mount.VolumeOptions{} } spec.VolumeOptions.NoCopy = !copyData } mp, err := p.parseMountSpec(spec, destRegex, convertTargetToBackslash, additionalValidators...) if mp != nil { mp.Mode = mode } if err != nil { err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err) } return mp, err } func (p *windowsParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) { return p.parseMountSpec(cfg, rxDestination, true, windowsSpecificValidators) } func (p *windowsParser) parseMountSpec(cfg mount.Mount, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) { if err := p.validateMountConfigReg(&cfg, destRegex, additionalValidators...); err != nil { return nil, err } mp := &MountPoint{ RW: !cfg.ReadOnly, Destination: cfg.Target, Type: cfg.Type, Spec: cfg, } if convertTargetToBackslash { mp.Destination = strings.Replace(cfg.Target, `/`, `\`, -1) } switch cfg.Type { case mount.TypeVolume: if cfg.Source == "" { mp.Name = stringid.GenerateNonCryptoID() } else { mp.Name = cfg.Source } mp.CopyData = p.DefaultCopyMode() if cfg.VolumeOptions != nil { if cfg.VolumeOptions.DriverConfig != nil { mp.Driver = cfg.VolumeOptions.DriverConfig.Name } if cfg.VolumeOptions.NoCopy { mp.CopyData = false } } case mount.TypeBind: mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1) case mount.TypeNamedPipe: mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1) } // cleanup trailing `\` except for paths like `c:\` if len(mp.Source) > 3 && mp.Source[len(mp.Source)-1] == '\\' { mp.Source = mp.Source[:len(mp.Source)-1] } if len(mp.Destination) > 3 && mp.Destination[len(mp.Destination)-1] == '\\' { mp.Destination = mp.Destination[:len(mp.Destination)-1] } return mp, nil } func (p *windowsParser) ParseVolumesFrom(spec string) (string, string, error) { if len(spec) == 0 { return "", "", fmt.Errorf("volumes-from specification cannot be an empty string") } specParts := strings.SplitN(spec, ":", 2) id := specParts[0] mode := "rw" if len(specParts) == 2 { mode = specParts[1] if !windowsValidMountMode(mode) { return "", "", errInvalidMode(mode) } // Do not allow copy modes on volumes-from if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet { return "", "", errInvalidMode(mode) } } return id, mode, nil } func (p *windowsParser) DefaultPropagationMode() mount.Propagation { return mount.Propagation("") } func (p *windowsParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) { return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS) } func (p *windowsParser) DefaultCopyMode() bool { return false } func (p *windowsParser) IsBackwardCompatible(m *MountPoint) bool { return false } func (p *windowsParser) ValidateTmpfsMountDestination(dest string) error { return errors.New("Platform does not support tmpfs") }