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 }