mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
384517218a
This utility was just a shallow wrapper around executing the regular expression, and in some cases, we didn't even use the error it returned, so better to inline the code instead of abstracting it away. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
459 lines
14 KiB
Go
459 lines
14 KiB
Go
package mounts // import "github.com/docker/docker/volume/mounts"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/mount"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
)
|
|
|
|
// NewWindowsParser creates a parser with Windows semantics.
|
|
func NewWindowsParser() Parser {
|
|
return &windowsParser{
|
|
fi: defaultFileInfoProvider{},
|
|
}
|
|
}
|
|
|
|
type windowsParser struct {
|
|
fi fileInfoProvider
|
|
}
|
|
|
|
const (
|
|
// Spec should be in the format [source:]destination[:mode]
|
|
//
|
|
// Examples: c:\foo bar:d:rw
|
|
// c:\foo:d:\bar
|
|
// myname:d:
|
|
// d:\
|
|
//
|
|
// Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See
|
|
// https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to
|
|
// test is https://regex-golang.appspot.com/assets/html/index.html
|
|
//
|
|
// Useful link for referencing named capturing groups:
|
|
// http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex
|
|
//
|
|
// There are three match groups: source, destination and mode.
|
|
//
|
|
|
|
// rxHostDir is the first option of a source
|
|
rxHostDir = `(?:\\\\\?\\)?[a-z]:[\\/](?:[^\\/:*?"<>|\r\n]+[\\/]?)*`
|
|
// rxName is the second option of a source
|
|
rxName = `[^\\/:*?"<>|\r\n]+`
|
|
|
|
// RXReservedNames are reserved names not possible on Windows
|
|
rxReservedNames = `(con)|(prn)|(nul)|(aux)|(com[1-9])|(lpt[1-9])`
|
|
|
|
// rxPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \)
|
|
rxPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+`
|
|
// rxSource is the combined possibilities for a source
|
|
rxSource = `((?P<source>((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?`
|
|
|
|
// Source. Can be either a host directory, a name, or omitted:
|
|
// HostDir:
|
|
// - Essentially using the folder solution from
|
|
// https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
|
|
// but adding case insensitivity.
|
|
// - Must be an absolute path such as c:\path
|
|
// - Can include spaces such as `c:\program files`
|
|
// - And then followed by a colon which is not in the capture group
|
|
// - And can be optional
|
|
// Name:
|
|
// - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
|
|
// - And then followed by a colon which is not in the capture group
|
|
// - And can be optional
|
|
|
|
// rxDestination is the regex expression for the mount destination
|
|
rxDestination = `(?P<destination>((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))`
|
|
|
|
// rxMode is the regex expression for the mode of the mount
|
|
// Mode (optional):
|
|
// - Hopefully self explanatory in comparison to above regex's.
|
|
// - Colon is not in the capture group
|
|
rxMode = `(:(?P<mode>(?i)ro|rw))?`
|
|
)
|
|
|
|
var (
|
|
volumeNameRegexp = regexp.MustCompile(`^` + rxName + `$`)
|
|
reservedNameRegexp = regexp.MustCompile(`^` + rxReservedNames + `$`)
|
|
hostDirRegexp = regexp.MustCompile(`^` + rxHostDir + `$`)
|
|
mountDestinationRegexp = regexp.MustCompile(`^` + rxDestination + `$`)
|
|
windowsSplitRawSpecRegexp = regexp.MustCompile(`^` + rxSource + rxDestination + rxMode + `$`)
|
|
)
|
|
|
|
type mountValidator func(mnt *mount.Mount) error
|
|
|
|
func (p *windowsParser) splitRawSpec(raw string, splitRegexp *regexp.Regexp) ([]string, error) {
|
|
match := splitRegexp.FindStringSubmatch(strings.ToLower(raw))
|
|
if len(match) == 0 {
|
|
return nil, errInvalidSpec(raw)
|
|
}
|
|
|
|
var split []string
|
|
matchgroups := make(map[string]string)
|
|
// Pull out the sub expressions from the named capture groups
|
|
for i, name := range splitRegexp.SubexpNames() {
|
|
matchgroups[name] = strings.ToLower(match[i])
|
|
}
|
|
if source, exists := matchgroups["source"]; exists {
|
|
if source != "" {
|
|
split = append(split, source)
|
|
}
|
|
}
|
|
if destination, exists := matchgroups["destination"]; exists {
|
|
if destination != "" {
|
|
split = append(split, destination)
|
|
}
|
|
}
|
|
if mode, exists := matchgroups["mode"]; exists {
|
|
if mode != "" {
|
|
split = append(split, mode)
|
|
}
|
|
}
|
|
// Fix #26329. If the destination appears to be a file, and the source is null,
|
|
// it may be because we've fallen through the possible naming regex and hit a
|
|
// situation where the user intention was to map a file into a container through
|
|
// a local volume, but this is not supported by the platform.
|
|
if matchgroups["source"] == "" && matchgroups["destination"] != "" {
|
|
if volumeNameRegexp.MatchString(matchgroups["destination"]) {
|
|
if reservedNameRegexp.MatchString(matchgroups["destination"]) {
|
|
return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"])
|
|
}
|
|
} else {
|
|
exists, isDir, _ := p.fi.fileInfo(matchgroups["destination"])
|
|
if exists && !isDir {
|
|
return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
|
|
|
|
}
|
|
}
|
|
}
|
|
return split, nil
|
|
}
|
|
|
|
func windowsValidMountMode(mode string) bool {
|
|
if mode == "" {
|
|
return true
|
|
}
|
|
// TODO should windows mounts produce an error if any mode was provided (they're a no-op on windows)
|
|
return rwModes[strings.ToLower(mode)]
|
|
}
|
|
|
|
func windowsValidateNotRoot(p string) error {
|
|
p = strings.ToLower(strings.Replace(p, `/`, `\`, -1))
|
|
if p == "c:" || p == `c:\` {
|
|
return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var windowsValidators mountValidator = func(m *mount.Mount) error {
|
|
if err := windowsValidateNotRoot(m.Target); err != nil {
|
|
return err
|
|
}
|
|
if !mountDestinationRegexp.MatchString(strings.ToLower(m.Target)) {
|
|
return fmt.Errorf("invalid mount path: '%s'", m.Target)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func windowsValidateAbsolute(p string) error {
|
|
if !mountDestinationRegexp.MatchString(strings.ToLower(p)) {
|
|
return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func windowsDetectMountType(p string) mount.Type {
|
|
if strings.HasPrefix(p, `\\.\pipe\`) {
|
|
return mount.TypeNamedPipe
|
|
} else if hostDirRegexp.MatchString(p) {
|
|
return mount.TypeBind
|
|
} else {
|
|
return mount.TypeVolume
|
|
}
|
|
}
|
|
|
|
func (p *windowsParser) ReadWrite(mode string) bool {
|
|
return strings.ToLower(mode) != "ro"
|
|
}
|
|
|
|
// ValidateVolumeName checks a volume name in a platform specific manner.
|
|
func (p *windowsParser) ValidateVolumeName(name string) error {
|
|
if !volumeNameRegexp.MatchString(name) {
|
|
return errors.New("invalid volume name")
|
|
}
|
|
if reservedNameRegexp.MatchString(name) {
|
|
return fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
|
|
}
|
|
return nil
|
|
}
|
|
func (p *windowsParser) ValidateMountConfig(mnt *mount.Mount) error {
|
|
return p.validateMountConfigReg(mnt, windowsValidators)
|
|
}
|
|
|
|
type fileInfoProvider interface {
|
|
fileInfo(path string) (exist, isDir bool, err error)
|
|
}
|
|
|
|
type defaultFileInfoProvider struct {
|
|
}
|
|
|
|
func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return false, false, err
|
|
}
|
|
return false, false, nil
|
|
}
|
|
return true, fi.IsDir(), nil
|
|
}
|
|
|
|
func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, additionalValidators ...mountValidator) error {
|
|
if len(mnt.Target) == 0 {
|
|
return &errMountConfig{mnt, errMissingField("Target")}
|
|
}
|
|
for _, v := range additionalValidators {
|
|
if err := v(mnt); err != nil {
|
|
return &errMountConfig{mnt, err}
|
|
}
|
|
}
|
|
|
|
switch mnt.Type {
|
|
case mount.TypeBind:
|
|
if len(mnt.Source) == 0 {
|
|
return &errMountConfig{mnt, errMissingField("Source")}
|
|
}
|
|
// Don't error out just because the propagation mode is not supported on the platform
|
|
if opts := mnt.BindOptions; opts != nil {
|
|
if len(opts.Propagation) > 0 {
|
|
return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
|
|
}
|
|
}
|
|
if mnt.VolumeOptions != nil {
|
|
return &errMountConfig{mnt, errExtraField("VolumeOptions")}
|
|
}
|
|
|
|
if err := windowsValidateAbsolute(mnt.Source); err != nil {
|
|
return &errMountConfig{mnt, err}
|
|
}
|
|
|
|
exists, isdir, err := p.fi.fileInfo(mnt.Source)
|
|
if err != nil {
|
|
return &errMountConfig{mnt, err}
|
|
}
|
|
if !exists {
|
|
return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
|
|
}
|
|
if !isdir {
|
|
return &errMountConfig{mnt, fmt.Errorf("source path must be a directory")}
|
|
}
|
|
|
|
case mount.TypeVolume:
|
|
if mnt.BindOptions != nil {
|
|
return &errMountConfig{mnt, errExtraField("BindOptions")}
|
|
}
|
|
|
|
if len(mnt.Source) == 0 && mnt.ReadOnly {
|
|
return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
|
|
}
|
|
|
|
if len(mnt.Source) != 0 {
|
|
if err := p.ValidateVolumeName(mnt.Source); err != nil {
|
|
return &errMountConfig{mnt, err}
|
|
}
|
|
}
|
|
case mount.TypeNamedPipe:
|
|
if len(mnt.Source) == 0 {
|
|
return &errMountConfig{mnt, errMissingField("Source")}
|
|
}
|
|
|
|
if mnt.BindOptions != nil {
|
|
return &errMountConfig{mnt, errExtraField("BindOptions")}
|
|
}
|
|
|
|
if mnt.ReadOnly {
|
|
return &errMountConfig{mnt, errExtraField("ReadOnly")}
|
|
}
|
|
|
|
if windowsDetectMountType(mnt.Source) != mount.TypeNamedPipe {
|
|
return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)}
|
|
}
|
|
|
|
if windowsDetectMountType(mnt.Target) != mount.TypeNamedPipe {
|
|
return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)}
|
|
}
|
|
default:
|
|
return &errMountConfig{mnt, errors.New("mount type unknown")}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *windowsParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
|
|
arr, err := p.splitRawSpec(raw, windowsSplitRawSpecRegexp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.parseMount(arr, raw, volumeDriver, true, windowsValidators)
|
|
}
|
|
|
|
func (p *windowsParser) parseMount(arr []string, raw, volumeDriver string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
|
|
var spec mount.Mount
|
|
var mode string
|
|
switch len(arr) {
|
|
case 1:
|
|
// Just a destination path in the container
|
|
spec.Target = arr[0]
|
|
case 2:
|
|
if windowsValidMountMode(arr[1]) {
|
|
// Destination + Mode is not a valid volume - volumes
|
|
// cannot include a mode. e.g. /foo:rw
|
|
return nil, errInvalidSpec(raw)
|
|
}
|
|
// Host Source Path or Name + Destination
|
|
spec.Source = strings.Replace(arr[0], `/`, `\`, -1)
|
|
spec.Target = arr[1]
|
|
case 3:
|
|
// HostSourcePath+DestinationPath+Mode
|
|
spec.Source = strings.Replace(arr[0], `/`, `\`, -1)
|
|
spec.Target = arr[1]
|
|
mode = arr[2]
|
|
default:
|
|
return nil, errInvalidSpec(raw)
|
|
}
|
|
if convertTargetToBackslash {
|
|
spec.Target = strings.Replace(spec.Target, `/`, `\`, -1)
|
|
}
|
|
|
|
if !windowsValidMountMode(mode) {
|
|
return nil, errInvalidMode(mode)
|
|
}
|
|
|
|
spec.Type = windowsDetectMountType(spec.Source)
|
|
spec.ReadOnly = !p.ReadWrite(mode)
|
|
|
|
// cannot assume that if a volume driver is passed in that we should set it
|
|
if volumeDriver != "" && spec.Type == mount.TypeVolume {
|
|
spec.VolumeOptions = &mount.VolumeOptions{
|
|
DriverConfig: &mount.Driver{Name: volumeDriver},
|
|
}
|
|
}
|
|
|
|
if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
|
|
if spec.VolumeOptions == nil {
|
|
spec.VolumeOptions = &mount.VolumeOptions{}
|
|
}
|
|
spec.VolumeOptions.NoCopy = !copyData
|
|
}
|
|
|
|
mp, err := p.parseMountSpec(spec, convertTargetToBackslash, additionalValidators...)
|
|
if mp != nil {
|
|
mp.Mode = mode
|
|
}
|
|
if err != nil {
|
|
err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
|
|
}
|
|
return mp, err
|
|
}
|
|
|
|
func (p *windowsParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
|
|
return p.parseMountSpec(cfg, true, windowsValidators)
|
|
}
|
|
|
|
func (p *windowsParser) parseMountSpec(cfg mount.Mount, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
|
|
if err := p.validateMountConfigReg(&cfg, additionalValidators...); err != nil {
|
|
return nil, err
|
|
}
|
|
mp := &MountPoint{
|
|
RW: !cfg.ReadOnly,
|
|
Destination: cfg.Target,
|
|
Type: cfg.Type,
|
|
Spec: cfg,
|
|
}
|
|
if convertTargetToBackslash {
|
|
mp.Destination = strings.Replace(cfg.Target, `/`, `\`, -1)
|
|
}
|
|
|
|
switch cfg.Type {
|
|
case mount.TypeVolume:
|
|
if cfg.Source == "" {
|
|
mp.Name = stringid.GenerateRandomID()
|
|
} else {
|
|
mp.Name = cfg.Source
|
|
}
|
|
mp.CopyData = p.DefaultCopyMode()
|
|
|
|
if cfg.VolumeOptions != nil {
|
|
if cfg.VolumeOptions.DriverConfig != nil {
|
|
mp.Driver = cfg.VolumeOptions.DriverConfig.Name
|
|
}
|
|
if cfg.VolumeOptions.NoCopy {
|
|
mp.CopyData = false
|
|
}
|
|
}
|
|
case mount.TypeBind:
|
|
mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1)
|
|
case mount.TypeNamedPipe:
|
|
mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1)
|
|
}
|
|
// cleanup trailing `\` except for paths like `c:\`
|
|
if len(mp.Source) > 3 && mp.Source[len(mp.Source)-1] == '\\' {
|
|
mp.Source = mp.Source[:len(mp.Source)-1]
|
|
}
|
|
if len(mp.Destination) > 3 && mp.Destination[len(mp.Destination)-1] == '\\' {
|
|
mp.Destination = mp.Destination[:len(mp.Destination)-1]
|
|
}
|
|
return mp, nil
|
|
}
|
|
|
|
func (p *windowsParser) ParseVolumesFrom(spec string) (string, string, error) {
|
|
if len(spec) == 0 {
|
|
return "", "", fmt.Errorf("volumes-from specification cannot be an empty string")
|
|
}
|
|
|
|
specParts := strings.SplitN(spec, ":", 2)
|
|
id := specParts[0]
|
|
mode := "rw"
|
|
|
|
if len(specParts) == 2 {
|
|
mode = specParts[1]
|
|
if !windowsValidMountMode(mode) {
|
|
return "", "", errInvalidMode(mode)
|
|
}
|
|
|
|
// Do not allow copy modes on volumes-from
|
|
if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
|
|
return "", "", errInvalidMode(mode)
|
|
}
|
|
}
|
|
return id, mode, nil
|
|
}
|
|
|
|
func (p *windowsParser) DefaultPropagationMode() mount.Propagation {
|
|
return ""
|
|
}
|
|
|
|
func (p *windowsParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) {
|
|
return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS)
|
|
}
|
|
|
|
func (p *windowsParser) DefaultCopyMode() bool {
|
|
return false
|
|
}
|
|
|
|
func (p *windowsParser) IsBackwardCompatible(m *MountPoint) bool {
|
|
return false
|
|
}
|
|
|
|
func (p *windowsParser) ValidateTmpfsMountDestination(dest string) error {
|
|
return errors.New("platform does not support tmpfs")
|
|
}
|
|
|
|
func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool {
|
|
return false
|
|
}
|