summaryrefslogtreecommitdiff
path: root/volume/volume_windows.go
blob: 22f6fc7a1498c128bc52ec4c993eabc7f8d0dc5d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package volume

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// read-write modes
var rwModes = map[string]bool{
	"rw": true,
}

// read-only modes
var roModes = map[string]bool{
	"ro": true,
}

var platformRawValidationOpts = []func(*validateOpts){
	// filepath.IsAbs is weird on Windows:
	//	`c:` is not considered an absolute path
	//	`c:\` is considered an absolute path
	// In any case, the regex matching below ensures absolute paths
	// TODO: consider this a bug with filepath.IsAbs (?)
	func(o *validateOpts) { o.skipAbsolutePathCheck = true },
}

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])`

	// RXSource is the combined possibilities for a source
	RXSource = `((?P<source>((` + RXHostDir + `)|(` + RXName + `))):)?`

	// 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]+)*\\?))`
	// Destination (aka container path):
	//    -  Variation on hostdir but can be a drive followed by colon as well
	//    -  If a path, must be absolute. Can include spaces
	//    -  Drive cannot be c: (explicitly checked in code, not RegEx)

	// 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))?`
)

// BackwardsCompatible decides whether this mount point can be
// used in old versions of Docker or not.
// Windows volumes are never backwards compatible.
func (m *MountPoint) BackwardsCompatible() bool {
	return false
}

func splitRawSpec(raw string) ([]string, error) {
	specExp := regexp.MustCompile(`^` + RXSource + RXDestination + RXMode + `$`)
	match := specExp.FindStringSubmatch(strings.ToLower(raw))

	// Must have something back
	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 specExp.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"] != "" {
		validName, err := IsVolumeNameValid(matchgroups["destination"])
		if err != nil {
			return nil, err
		}
		if !validName {
			if fi, err := os.Stat(matchgroups["destination"]); err == nil {
				if !fi.IsDir() {
					return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
				}
			}
		}
	}
	return split, nil
}

// IsVolumeNameValid checks a volume name in a platform specific manner.
func IsVolumeNameValid(name string) (bool, error) {
	nameExp := regexp.MustCompile(`^` + RXName + `$`)
	if !nameExp.MatchString(name) {
		return false, nil
	}
	nameExp = regexp.MustCompile(`^` + RXReservedNames + `$`)
	if nameExp.MatchString(name) {
		return false, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
	}
	return true, nil
}

// ValidMountMode will make sure the mount mode is valid.
// returns if it's a valid mount mode or not.
func ValidMountMode(mode string) bool {
	if mode == "" {
		return true
	}
	return roModes[strings.ToLower(mode)] || rwModes[strings.ToLower(mode)]
}

// ReadWrite tells you if a mode string is a valid read-write mode or not.
func ReadWrite(mode string) bool {
	return rwModes[strings.ToLower(mode)] || mode == ""
}

func validateNotRoot(p string) error {
	p = strings.ToLower(convertSlash(p))
	if p == "c:" || p == `c:\` {
		return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
	}
	return nil
}

func validateCopyMode(mode bool) error {
	if mode {
		return fmt.Errorf("Windows does not support copying image path content")
	}
	return nil
}

func convertSlash(p string) string {
	return filepath.FromSlash(p)
}

func clean(p string) string {
	if match, _ := regexp.MatchString("^[a-z]:$", p); match {
		return p
	}
	return filepath.Clean(p)
}

func validateStat(fi os.FileInfo) error {
	if !fi.IsDir() {
		return fmt.Errorf("source path must be a directory")
	}
	return nil
}