| package mounts // import "github.com/docker/docker/volume/mounts" | |
| import ( | |
| "fmt" | |
| "strings" | |
| "testing" | |
| "github.com/docker/docker/api/types/mount" | |
| "gotest.tools/v3/assert" | |
| ) | |
| func TestWindowsParseMountRaw(t *testing.T) { | |
| valid := []string{ | |
| `d:\`, | |
| `d:`, | |
| `d:\path`, | |
| `d:\path with space`, | |
| `c:\:d:\`, | |
| `c:\windows\:d:`, | |
| `c:\windows:d:\s p a c e`, | |
| `c:\windows:d:\s p a c e:RW`, | |
| `c:\program files:d:\s p a c e i n h o s t d i r`, | |
| `0123456789name:d:`, | |
| `MiXeDcAsEnAmE:d:`, | |
| `test-aux-volume:d:`, // includes reserved word, but is not one itself | |
| `name:D:`, | |
| `name:D::rW`, | |
| `name:D::RW`, | |
| `name:D::RO`, | |
| `c:/:d:/forward/slashes/are/good/too`, | |
| `c:/:d:/including with/spaces:ro`, | |
| `c:\Windows`, // With capital | |
| `c:\Program Files (x86)`, // With capitals and brackets | |
| `\\?\c:\windows\:d:`, // Long path handling (source) | |
| `c:\windows\:\\?\d:\`, // Long path handling (target) | |
| `\\.\pipe\foo:\\.\pipe\foo`, // named pipe | |
| `//./pipe/foo://./pipe/foo`, // named pipe forward slashes | |
| } | |
| invalid := map[string]string{ | |
| ``: "invalid volume specification: ", | |
| `.`: "invalid volume specification: ", | |
| `..\`: "invalid volume specification: ", | |
| `c:\:..\`: "invalid volume specification: ", | |
| `c:\:d:\:xyzzy`: "invalid volume specification: ", | |
| `c:`: "cannot be `c:`", | |
| `c:\`: "cannot be `c:`", | |
| `c:\notexist:d:`: `source path does not exist: c:\notexist`, | |
| `c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`, | |
| `name<:d:`: `invalid volume specification`, | |
| `name>:d:`: `invalid volume specification`, | |
| `name::d:`: `invalid volume specification`, | |
| `name":d:`: `invalid volume specification`, | |
| `name\:d:`: `invalid volume specification`, | |
| `name*:d:`: `invalid volume specification`, | |
| `name|:d:`: `invalid volume specification`, | |
| `name?:d:`: `invalid volume specification`, | |
| `name/:d:`: `invalid volume specification`, | |
| `d:\pathandmode:rw`: `invalid volume specification`, | |
| `d:\pathandmode:ro`: `invalid volume specification`, | |
| `con:d:`: `cannot be a reserved word for Windows filenames`, | |
| `PRN:d:`: `cannot be a reserved word for Windows filenames`, | |
| `aUx:d:`: `cannot be a reserved word for Windows filenames`, | |
| `nul:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com1:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com2:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com3:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com4:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com5:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com6:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com7:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com8:d:`: `cannot be a reserved word for Windows filenames`, | |
| `com9:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt1:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt2:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt3:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt4:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt5:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt6:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt7:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt8:d:`: `cannot be a reserved word for Windows filenames`, | |
| `lpt9:d:`: `cannot be a reserved word for Windows filenames`, | |
| `c:\windows\system32\ntdll.dll`: `Only directories can be mapped on this platform`, | |
| `\\.\pipe\foo:c:\pipe`: `'c:\pipe' is not a valid pipe path`, | |
| } | |
| parser := NewWindowsParser() | |
| if p, ok := parser.(*windowsParser); ok { | |
| p.fi = mockFiProvider{} | |
| } | |
| for _, path := range valid { | |
| if _, err := parser.ParseMountRaw(path, "local"); err != nil { | |
| t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err) | |
| } | |
| } | |
| for path, expectedError := range invalid { | |
| if mp, err := parser.ParseMountRaw(path, "local"); err == nil { | |
| t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp) | |
| } else { | |
| if !strings.Contains(err.Error(), expectedError) { | |
| t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) | |
| } | |
| } | |
| } | |
| } | |
| func TestWindowsParseMountRawSplit(t *testing.T) { | |
| cases := []struct { | |
| bind string | |
| driver string | |
| expType mount.Type | |
| expDest string | |
| expSource string | |
| expName string | |
| expDriver string | |
| expRW bool | |
| fail bool | |
| }{ | |
| {`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false}, | |
| {`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, | |
| {`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false}, | |
| {`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, | |
| {`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true}, | |
| {`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, | |
| {`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, | |
| {`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false}, | |
| {`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, | |
| {`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, | |
| {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false}, | |
| {`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, | |
| {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, | |
| } | |
| parser := NewWindowsParser() | |
| if p, ok := parser.(*windowsParser); ok { | |
| p.fi = mockFiProvider{} | |
| } | |
| for i, c := range cases { | |
| c := c | |
| t.Run(fmt.Sprintf("%d_%s", i, c.bind), func(t *testing.T) { | |
| m, err := parser.ParseMountRaw(c.bind, c.driver) | |
| if c.fail { | |
| assert.ErrorContains(t, err, "", "expected an error") | |
| return | |
| } | |
| assert.NilError(t, err) | |
| assert.Equal(t, m.Destination, c.expDest) | |
| assert.Equal(t, m.Source, c.expSource) | |
| assert.Equal(t, m.Name, c.expName) | |
| assert.Equal(t, m.Driver, c.expDriver) | |
| assert.Equal(t, m.RW, c.expRW) | |
| assert.Equal(t, m.Type, c.expType) | |
| }) | |
| } | |
| } | |
| // TestWindowsParseMountSpecBindWithFileinfoError makes sure that the parser returns | |
| // the error produced by the fileinfo provider. | |
| // | |
| // Some extra context for the future in case of changes and possible wtf are we | |
| // testing this for: | |
| // | |
| // Currently this "fileInfoProvider" returns (bool, bool, error) | |
| // The 1st bool is "does this path exist" | |
| // The 2nd bool is "is this path a dir" | |
| // Then of course the error is an error. | |
| // | |
| // The issue is the parser was ignoring the error and only looking at the | |
| // "does this path exist" boolean, which is always false if there is an error. | |
| // Then the error returned to the caller was a (slightly, maybe) friendlier | |
| // error string than what comes from `os.Stat` | |
| // So ...the caller was always getting an error saying the path doesn't exist | |
| // even if it does exist but got some other error (like a permission error). | |
| // This is confusing to users. | |
| func TestWindowsParseMountSpecBindWithFileinfoError(t *testing.T) { | |
| parser := NewWindowsParser() | |
| testErr := fmt.Errorf("some crazy error") | |
| if pr, ok := parser.(*windowsParser); ok { | |
| pr.fi = &mockFiProviderWithError{err: testErr} | |
| } | |
| _, err := parser.ParseMountSpec(mount.Mount{ | |
| Type: mount.TypeBind, | |
| Source: `c:\bananas`, | |
| Target: `c:\bananas`, | |
| }) | |
| assert.ErrorContains(t, err, testErr.Error()) | |
| } |