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 }