github.com/moby/docker@v26.1.3+incompatible/volume/mounts/windows_parser.go (about)

     1  package mounts // import "github.com/docker/docker/volume/mounts"
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"github.com/docker/docker/api/types/mount"
    13  	"github.com/docker/docker/pkg/stringid"
    14  )
    15  
    16  // NewWindowsParser creates a parser with Windows semantics.
    17  func NewWindowsParser() Parser {
    18  	return &windowsParser{
    19  		fi: defaultFileInfoProvider{},
    20  	}
    21  }
    22  
    23  type windowsParser struct {
    24  	fi fileInfoProvider
    25  }
    26  
    27  const (
    28  	// Spec should be in the format [source:]destination[:mode]
    29  	//
    30  	// Examples: c:\foo bar:d:rw
    31  	//           c:\foo:d:\bar
    32  	//           myname:d:
    33  	//           d:\
    34  	//
    35  	// Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See
    36  	// https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to
    37  	// test is https://regex-golang.appspot.com/assets/html/index.html
    38  	//
    39  	// Useful link for referencing named capturing groups:
    40  	// http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex
    41  	//
    42  	// There are three match groups: source, destination and mode.
    43  	//
    44  
    45  	// rxHostDir is the first option of a source
    46  	rxHostDir = `(?:\\\\\?\\)?[a-z]:[\\/](?:[^\\/:*?"<>|\r\n]+[\\/]?)*`
    47  	// rxName is the second option of a source
    48  	rxName = `[^\\/:*?"<>|\r\n]+`
    49  
    50  	// RXReservedNames are reserved names not possible on Windows
    51  	rxReservedNames = `(con|prn|nul|aux|com[1-9]|lpt[1-9])`
    52  
    53  	// rxPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \)
    54  	rxPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+`
    55  	// rxSource is the combined possibilities for a source
    56  	rxSource = `((?P<source>((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?`
    57  
    58  	// Source. Can be either a host directory, a name, or omitted:
    59  	//  HostDir:
    60  	//    -  Essentially using the folder solution from
    61  	//       https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
    62  	//       but adding case insensitivity.
    63  	//    -  Must be an absolute path such as c:\path
    64  	//    -  Can include spaces such as `c:\program files`
    65  	//    -  And then followed by a colon which is not in the capture group
    66  	//    -  And can be optional
    67  	//  Name:
    68  	//    -  Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
    69  	//    -  And then followed by a colon which is not in the capture group
    70  	//    -  And can be optional
    71  
    72  	// rxDestination is the regex expression for the mount destination
    73  	rxDestination = `(?P<destination>((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))`
    74  
    75  	// rxMode is the regex expression for the mode of the mount
    76  	// Mode (optional):
    77  	//    -  Hopefully self explanatory in comparison to above regex's.
    78  	//    -  Colon is not in the capture group
    79  	rxMode = `(:(?P<mode>(?i)ro|rw))?`
    80  )
    81  
    82  var (
    83  	volumeNameRegexp          = regexp.MustCompile(`^` + rxName + `$`)
    84  	reservedNameRegexp        = regexp.MustCompile(`^` + rxReservedNames + `$`)
    85  	hostDirRegexp             = regexp.MustCompile(`^` + rxHostDir + `$`)
    86  	mountDestinationRegexp    = regexp.MustCompile(`^` + rxDestination + `$`)
    87  	windowsSplitRawSpecRegexp = regexp.MustCompile(`^` + rxSource + rxDestination + rxMode + `$`)
    88  )
    89  
    90  type mountValidator func(mnt *mount.Mount) error
    91  
    92  func (p *windowsParser) splitRawSpec(raw string, splitRegexp *regexp.Regexp) ([]string, error) {
    93  	match := splitRegexp.FindStringSubmatch(strings.ToLower(raw))
    94  	if len(match) == 0 {
    95  		return nil, errInvalidSpec(raw)
    96  	}
    97  
    98  	var split []string
    99  	matchgroups := make(map[string]string)
   100  	// Pull out the sub expressions from the named capture groups
   101  	for i, name := range splitRegexp.SubexpNames() {
   102  		matchgroups[name] = strings.ToLower(match[i])
   103  	}
   104  	if source, exists := matchgroups["source"]; exists {
   105  		if source != "" {
   106  			split = append(split, source)
   107  		}
   108  	}
   109  	if destination, exists := matchgroups["destination"]; exists {
   110  		if destination != "" {
   111  			split = append(split, destination)
   112  		}
   113  	}
   114  	if mode, exists := matchgroups["mode"]; exists {
   115  		if mode != "" {
   116  			split = append(split, mode)
   117  		}
   118  	}
   119  	// Fix #26329. If the destination appears to be a file, and the source is null,
   120  	// it may be because we've fallen through the possible naming regex and hit a
   121  	// situation where the user intention was to map a file into a container through
   122  	// a local volume, but this is not supported by the platform.
   123  	if matchgroups["source"] == "" && matchgroups["destination"] != "" {
   124  		if volumeNameRegexp.MatchString(matchgroups["destination"]) {
   125  			if reservedNameRegexp.MatchString(matchgroups["destination"]) {
   126  				return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"])
   127  			}
   128  		} else {
   129  			exists, isDir, _ := p.fi.fileInfo(matchgroups["destination"])
   130  			if exists && !isDir {
   131  				return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
   132  			}
   133  		}
   134  	}
   135  	return split, nil
   136  }
   137  
   138  func windowsValidMountMode(mode string) bool {
   139  	if mode == "" {
   140  		return true
   141  	}
   142  	// TODO should windows mounts produce an error if any mode was provided (they're a no-op on windows)
   143  	return rwModes[strings.ToLower(mode)]
   144  }
   145  
   146  func windowsValidateNotRoot(p string) error {
   147  	p = strings.ToLower(strings.ReplaceAll(p, `/`, `\`))
   148  	if p == "c:" || p == `c:\` {
   149  		return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
   150  	}
   151  	return nil
   152  }
   153  
   154  var windowsValidators mountValidator = func(m *mount.Mount) error {
   155  	if err := windowsValidateNotRoot(m.Target); err != nil {
   156  		return err
   157  	}
   158  	if !mountDestinationRegexp.MatchString(strings.ToLower(m.Target)) {
   159  		return fmt.Errorf("invalid mount path: '%s'", m.Target)
   160  	}
   161  	return nil
   162  }
   163  
   164  func windowsValidateAbsolute(p string) error {
   165  	if !mountDestinationRegexp.MatchString(strings.ToLower(p)) {
   166  		return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
   167  	}
   168  	return nil
   169  }
   170  
   171  func windowsDetectMountType(p string) mount.Type {
   172  	if strings.HasPrefix(p, `\\.\pipe\`) {
   173  		return mount.TypeNamedPipe
   174  	} else if hostDirRegexp.MatchString(p) {
   175  		return mount.TypeBind
   176  	} else {
   177  		return mount.TypeVolume
   178  	}
   179  }
   180  
   181  func (p *windowsParser) ReadWrite(mode string) bool {
   182  	return strings.ToLower(mode) != "ro"
   183  }
   184  
   185  // ValidateVolumeName checks a volume name in a platform specific manner.
   186  func (p *windowsParser) ValidateVolumeName(name string) error {
   187  	if !volumeNameRegexp.MatchString(name) {
   188  		return errors.New("invalid volume name")
   189  	}
   190  	if reservedNameRegexp.MatchString(name) {
   191  		return fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
   192  	}
   193  	return nil
   194  }
   195  
   196  func (p *windowsParser) ValidateMountConfig(mnt *mount.Mount) error {
   197  	return p.validateMountConfigReg(mnt, windowsValidators)
   198  }
   199  
   200  type fileInfoProvider interface {
   201  	fileInfo(path string) (exist, isDir bool, err error)
   202  }
   203  
   204  type defaultFileInfoProvider struct{}
   205  
   206  func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) {
   207  	fi, err := os.Stat(path)
   208  	if err != nil {
   209  		if !os.IsNotExist(err) {
   210  			return false, false, err
   211  		}
   212  		return false, false, nil
   213  	}
   214  	return true, fi.IsDir(), nil
   215  }
   216  
   217  func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, additionalValidators ...mountValidator) error {
   218  	if len(mnt.Target) == 0 {
   219  		return &errMountConfig{mnt, errMissingField("Target")}
   220  	}
   221  	for _, v := range additionalValidators {
   222  		if err := v(mnt); err != nil {
   223  			return &errMountConfig{mnt, err}
   224  		}
   225  	}
   226  
   227  	switch mnt.Type {
   228  	case mount.TypeBind:
   229  		if len(mnt.Source) == 0 {
   230  			return &errMountConfig{mnt, errMissingField("Source")}
   231  		}
   232  		// Don't error out just because the propagation mode is not supported on the platform
   233  		if opts := mnt.BindOptions; opts != nil {
   234  			if len(opts.Propagation) > 0 {
   235  				return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
   236  			}
   237  		}
   238  		if mnt.VolumeOptions != nil {
   239  			return &errMountConfig{mnt, errExtraField("VolumeOptions")}
   240  		}
   241  
   242  		if err := windowsValidateAbsolute(mnt.Source); err != nil {
   243  			return &errMountConfig{mnt, err}
   244  		}
   245  
   246  		exists, isdir, err := p.fi.fileInfo(mnt.Source)
   247  		if err != nil {
   248  			return &errMountConfig{mnt, err}
   249  		}
   250  		if !exists {
   251  			return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
   252  		}
   253  		if !isdir {
   254  			return &errMountConfig{mnt, fmt.Errorf("source path must be a directory")}
   255  		}
   256  
   257  	case mount.TypeVolume:
   258  		if mnt.BindOptions != nil {
   259  			return &errMountConfig{mnt, errExtraField("BindOptions")}
   260  		}
   261  
   262  		anonymousVolume := len(mnt.Source) == 0
   263  		if mnt.VolumeOptions != nil && mnt.VolumeOptions.Subpath != "" {
   264  			if anonymousVolume {
   265  				return errAnonymousVolumeWithSubpath
   266  			}
   267  
   268  			// Check if path is relative but without any back traversals
   269  			if !filepath.IsLocal(mnt.VolumeOptions.Subpath) {
   270  				return &errMountConfig{mnt, errInvalidSubpath}
   271  			}
   272  		}
   273  
   274  		if anonymousVolume && mnt.ReadOnly {
   275  			return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
   276  		}
   277  
   278  		if len(mnt.Source) != 0 {
   279  			if err := p.ValidateVolumeName(mnt.Source); err != nil {
   280  				return &errMountConfig{mnt, err}
   281  			}
   282  		}
   283  	case mount.TypeNamedPipe:
   284  		if len(mnt.Source) == 0 {
   285  			return &errMountConfig{mnt, errMissingField("Source")}
   286  		}
   287  
   288  		if mnt.BindOptions != nil {
   289  			return &errMountConfig{mnt, errExtraField("BindOptions")}
   290  		}
   291  
   292  		if mnt.ReadOnly {
   293  			return &errMountConfig{mnt, errExtraField("ReadOnly")}
   294  		}
   295  
   296  		if windowsDetectMountType(mnt.Source) != mount.TypeNamedPipe {
   297  			return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)}
   298  		}
   299  
   300  		if windowsDetectMountType(mnt.Target) != mount.TypeNamedPipe {
   301  			return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)}
   302  		}
   303  	default:
   304  		return &errMountConfig{mnt, errors.New("mount type unknown")}
   305  	}
   306  	return nil
   307  }
   308  
   309  func (p *windowsParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
   310  	arr, err := p.splitRawSpec(raw, windowsSplitRawSpecRegexp)
   311  	if err != nil {
   312  		return nil, err
   313  	}
   314  	return p.parseMount(arr, raw, volumeDriver, true, windowsValidators)
   315  }
   316  
   317  func (p *windowsParser) parseMount(arr []string, raw, volumeDriver string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
   318  	var spec mount.Mount
   319  	var mode string
   320  	switch len(arr) {
   321  	case 1:
   322  		// Just a destination path in the container
   323  		spec.Target = arr[0]
   324  	case 2:
   325  		if windowsValidMountMode(arr[1]) {
   326  			// Destination + Mode is not a valid volume - volumes
   327  			// cannot include a mode. e.g. /foo:rw
   328  			return nil, errInvalidSpec(raw)
   329  		}
   330  		// Host Source Path or Name + Destination
   331  		spec.Source = strings.ReplaceAll(arr[0], `/`, `\`)
   332  		spec.Target = arr[1]
   333  	case 3:
   334  		// HostSourcePath+DestinationPath+Mode
   335  		spec.Source = strings.ReplaceAll(arr[0], `/`, `\`)
   336  		spec.Target = arr[1]
   337  		mode = arr[2]
   338  	default:
   339  		return nil, errInvalidSpec(raw)
   340  	}
   341  	if convertTargetToBackslash {
   342  		spec.Target = strings.ReplaceAll(spec.Target, `/`, `\`)
   343  	}
   344  
   345  	if !windowsValidMountMode(mode) {
   346  		return nil, errInvalidMode(mode)
   347  	}
   348  
   349  	spec.Type = windowsDetectMountType(spec.Source)
   350  	spec.ReadOnly = !p.ReadWrite(mode)
   351  
   352  	// cannot assume that if a volume driver is passed in that we should set it
   353  	if volumeDriver != "" && spec.Type == mount.TypeVolume {
   354  		spec.VolumeOptions = &mount.VolumeOptions{
   355  			DriverConfig: &mount.Driver{Name: volumeDriver},
   356  		}
   357  	}
   358  
   359  	if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
   360  		if spec.VolumeOptions == nil {
   361  			spec.VolumeOptions = &mount.VolumeOptions{}
   362  		}
   363  		spec.VolumeOptions.NoCopy = !copyData
   364  	}
   365  
   366  	mp, err := p.parseMountSpec(spec, convertTargetToBackslash, additionalValidators...)
   367  	if mp != nil {
   368  		mp.Mode = mode
   369  	}
   370  	if err != nil {
   371  		err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
   372  	}
   373  	return mp, err
   374  }
   375  
   376  func (p *windowsParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
   377  	return p.parseMountSpec(cfg, true, windowsValidators)
   378  }
   379  
   380  func (p *windowsParser) parseMountSpec(cfg mount.Mount, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
   381  	if err := p.validateMountConfigReg(&cfg, additionalValidators...); err != nil {
   382  		return nil, err
   383  	}
   384  	mp := &MountPoint{
   385  		RW:          !cfg.ReadOnly,
   386  		Destination: cfg.Target,
   387  		Type:        cfg.Type,
   388  		Spec:        cfg,
   389  	}
   390  	if convertTargetToBackslash {
   391  		mp.Destination = strings.ReplaceAll(cfg.Target, `/`, `\`)
   392  	}
   393  
   394  	switch cfg.Type {
   395  	case mount.TypeVolume:
   396  		if cfg.Source == "" {
   397  			mp.Name = stringid.GenerateRandomID()
   398  		} else {
   399  			mp.Name = cfg.Source
   400  		}
   401  		mp.CopyData = p.DefaultCopyMode()
   402  
   403  		if cfg.VolumeOptions != nil {
   404  			if cfg.VolumeOptions.DriverConfig != nil {
   405  				mp.Driver = cfg.VolumeOptions.DriverConfig.Name
   406  			}
   407  			if cfg.VolumeOptions.NoCopy {
   408  				mp.CopyData = false
   409  			}
   410  		}
   411  	case mount.TypeBind:
   412  		mp.Source = strings.ReplaceAll(cfg.Source, `/`, `\`)
   413  	case mount.TypeNamedPipe:
   414  		mp.Source = strings.ReplaceAll(cfg.Source, `/`, `\`)
   415  	}
   416  	// cleanup trailing `\` except for paths like `c:\`
   417  	if len(mp.Source) > 3 && mp.Source[len(mp.Source)-1] == '\\' {
   418  		mp.Source = mp.Source[:len(mp.Source)-1]
   419  	}
   420  	if len(mp.Destination) > 3 && mp.Destination[len(mp.Destination)-1] == '\\' {
   421  		mp.Destination = mp.Destination[:len(mp.Destination)-1]
   422  	}
   423  	return mp, nil
   424  }
   425  
   426  func (p *windowsParser) ParseVolumesFrom(spec string) (string, string, error) {
   427  	if len(spec) == 0 {
   428  		return "", "", fmt.Errorf("volumes-from specification cannot be an empty string")
   429  	}
   430  
   431  	id, mode, _ := strings.Cut(spec, ":")
   432  	if mode == "" {
   433  		return id, "rw", nil
   434  	}
   435  
   436  	if !windowsValidMountMode(mode) {
   437  		return "", "", errInvalidMode(mode)
   438  	}
   439  
   440  	// Do not allow copy modes on volumes-from
   441  	if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
   442  		return "", "", errInvalidMode(mode)
   443  	}
   444  	return id, mode, nil
   445  }
   446  
   447  func (p *windowsParser) DefaultPropagationMode() mount.Propagation {
   448  	return ""
   449  }
   450  
   451  func (p *windowsParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) {
   452  	return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS)
   453  }
   454  
   455  func (p *windowsParser) DefaultCopyMode() bool {
   456  	return false
   457  }
   458  
   459  func (p *windowsParser) IsBackwardCompatible(m *MountPoint) bool {
   460  	return false
   461  }
   462  
   463  func (p *windowsParser) ValidateTmpfsMountDestination(dest string) error {
   464  	return errors.New("platform does not support tmpfs")
   465  }
   466  
   467  func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool {
   468  	return false
   469  }