| package mounts // import "github.com/docker/docker/volume/mounts" | |
| import ( | |
| "errors" | |
| "fmt" | |
| "os" | |
| "regexp" | |
| "runtime" | |
| "strings" | |
| "github.com/docker/docker/api/types/mount" | |
| "github.com/docker/docker/pkg/stringid" | |
| ) | |
| // NewWindowsParser creates a parser with Windows semantics. | |
| func NewWindowsParser() Parser { | |
| return &windowsParser{ | |
| fi: defaultFileInfoProvider{}, | |
| } | |
| } | |
| type windowsParser struct { | |
| fi fileInfoProvider | |
| } | |
| 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<source>((` + 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<destination>((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))` | |
| // 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<mode>(?i)ro|rw))?` | |
| ) | |
| var ( | |
| volumeNameRegexp = regexp.MustCompile(`^` + rxName + `$`) | |
| reservedNameRegexp = regexp.MustCompile(`^` + rxReservedNames + `$`) | |
| hostDirRegexp = regexp.MustCompile(`^` + rxHostDir + `$`) | |
| mountDestinationRegexp = regexp.MustCompile(`^` + rxDestination + `$`) | |
| windowsSplitRawSpecRegexp = regexp.MustCompile(`^` + rxSource + rxDestination + rxMode + `$`) | |
| ) | |
| type mountValidator func(mnt *mount.Mount) error | |
| func (p *windowsParser) splitRawSpec(raw string, splitRegexp *regexp.Regexp) ([]string, error) { | |
| match := splitRegexp.FindStringSubmatch(strings.ToLower(raw)) | |
| 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 splitRegexp.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"] != "" { | |
| if volumeNameRegexp.MatchString(matchgroups["destination"]) { | |
| if reservedNameRegexp.MatchString(matchgroups["destination"]) { | |
| return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"]) | |
| } | |
| } else { | |
| exists, isDir, _ := p.fi.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 | |
| } | |
| // TODO should windows mounts produce an error if any mode was provided (they're a no-op on windows) | |
| return rwModes[strings.ToLower(mode)] | |
| } | |
| func windowsValidateNotRoot(p string) error { | |
| p = strings.ToLower(strings.ReplaceAll(p, `/`, `\`)) | |
| if p == "c:" || p == `c:\` { | |
| return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p) | |
| } | |
| return nil | |
| } | |
| var windowsValidators mountValidator = func(m *mount.Mount) error { | |
| if err := windowsValidateNotRoot(m.Target); err != nil { | |
| return err | |
| } | |
| if !mountDestinationRegexp.MatchString(strings.ToLower(m.Target)) { | |
| return fmt.Errorf("invalid mount path: '%s'", m.Target) | |
| } | |
| return nil | |
| } | |
| func windowsValidateAbsolute(p string) error { | |
| if !mountDestinationRegexp.MatchString(strings.ToLower(p)) { | |
| 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 hostDirRegexp.MatchString(p) { | |
| return mount.TypeBind | |
| } else { | |
| return mount.TypeVolume | |
| } | |
| } | |
| func (p *windowsParser) ReadWrite(mode string) bool { | |
| return strings.ToLower(mode) != "ro" | |
| } | |
| // ValidateVolumeName checks a volume name in a platform specific manner. | |
| func (p *windowsParser) ValidateVolumeName(name string) error { | |
| if !volumeNameRegexp.MatchString(name) { | |
| return errors.New("invalid volume name") | |
| } | |
| if reservedNameRegexp.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, windowsValidators) | |
| } | |
| 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 | |
| } | |
| func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, additionalValidators ...mountValidator) error { | |
| if len(mnt.Target) == 0 { | |
| return &errMountConfig{mnt, errMissingField("Target")} | |
| } | |
| for _, v := range additionalValidators { | |
| if err := v(mnt); 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 := p.fi.fileInfo(mnt.Source) | |
| if err != nil { | |
| return &errMountConfig{mnt, err} | |
| } | |
| if !exists { | |
| return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)} | |
| } | |
| 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) { | |
| arr, err := p.splitRawSpec(raw, windowsSplitRawSpecRegexp) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return p.parseMount(arr, raw, volumeDriver, true, windowsValidators) | |
| } | |
| func (p *windowsParser) parseMount(arr []string, raw, volumeDriver string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) { | |
| 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.ReplaceAll(arr[0], `/`, `\`) | |
| spec.Target = arr[1] | |
| case 3: | |
| // HostSourcePath+DestinationPath+Mode | |
| spec.Source = strings.ReplaceAll(arr[0], `/`, `\`) | |
| spec.Target = arr[1] | |
| mode = arr[2] | |
| default: | |
| return nil, errInvalidSpec(raw) | |
| } | |
| if convertTargetToBackslash { | |
| spec.Target = strings.ReplaceAll(spec.Target, `/`, `\`) | |
| } | |
| 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, 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, true, windowsValidators) | |
| } | |
| func (p *windowsParser) parseMountSpec(cfg mount.Mount, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) { | |
| if err := p.validateMountConfigReg(&cfg, additionalValidators...); err != nil { | |
| return nil, err | |
| } | |
| mp := &MountPoint{ | |
| RW: !cfg.ReadOnly, | |
| Destination: cfg.Target, | |
| Type: cfg.Type, | |
| Spec: cfg, | |
| } | |
| if convertTargetToBackslash { | |
| mp.Destination = strings.ReplaceAll(cfg.Target, `/`, `\`) | |
| } | |
| switch cfg.Type { | |
| case mount.TypeVolume: | |
| if cfg.Source == "" { | |
| mp.Name = stringid.GenerateRandomID() | |
| } 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.ReplaceAll(cfg.Source, `/`, `\`) | |
| case mount.TypeNamedPipe: | |
| mp.Source = strings.ReplaceAll(cfg.Source, `/`, `\`) | |
| } | |
| // 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") | |
| } | |
| id, mode, _ := strings.Cut(spec, ":") | |
| if mode == "" { | |
| return id, "rw", nil | |
| } | |
| 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 "" | |
| } | |
| 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") | |
| } | |
| func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool { | |
| return false | |
| } |