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