github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/volume/mounts/windows_parser.go (about)

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