github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/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 specParts := strings.SplitN(spec, ":", 2) 419 id := specParts[0] 420 mode := "rw" 421 422 if len(specParts) == 2 { 423 mode = specParts[1] 424 if !windowsValidMountMode(mode) { 425 return "", "", errInvalidMode(mode) 426 } 427 428 // Do not allow copy modes on volumes-from 429 if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet { 430 return "", "", errInvalidMode(mode) 431 } 432 } 433 return id, mode, nil 434 } 435 436 func (p *windowsParser) DefaultPropagationMode() mount.Propagation { 437 return "" 438 } 439 440 func (p *windowsParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) { 441 return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS) 442 } 443 444 func (p *windowsParser) DefaultCopyMode() bool { 445 return false 446 } 447 448 func (p *windowsParser) IsBackwardCompatible(m *MountPoint) bool { 449 return false 450 } 451 452 func (p *windowsParser) ValidateTmpfsMountDestination(dest string) error { 453 return errors.New("platform does not support tmpfs") 454 } 455 456 func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool { 457 return false 458 }