github.com/LazyboyChen7/engine@v17.12.1-ce-rc2+incompatible/volume/volume_test.go (about) 1 package volume 2 3 import ( 4 "io/ioutil" 5 "os" 6 "runtime" 7 "strings" 8 "testing" 9 10 "github.com/docker/docker/api/types/mount" 11 ) 12 13 type parseMountRawTestSet struct { 14 valid []string 15 invalid map[string]string 16 } 17 18 func TestConvertTmpfsOptions(t *testing.T) { 19 type testCase struct { 20 opt mount.TmpfsOptions 21 readOnly bool 22 expectedSubstrings []string 23 unexpectedSubstrings []string 24 } 25 cases := []testCase{ 26 { 27 opt: mount.TmpfsOptions{SizeBytes: 1024 * 1024, Mode: 0700}, 28 readOnly: false, 29 expectedSubstrings: []string{"size=1m", "mode=700"}, 30 unexpectedSubstrings: []string{"ro"}, 31 }, 32 { 33 opt: mount.TmpfsOptions{}, 34 readOnly: true, 35 expectedSubstrings: []string{"ro"}, 36 unexpectedSubstrings: []string{}, 37 }, 38 } 39 p := &linuxParser{} 40 for _, c := range cases { 41 data, err := p.ConvertTmpfsOptions(&c.opt, c.readOnly) 42 if err != nil { 43 t.Fatalf("could not convert %+v (readOnly: %v) to string: %v", 44 c.opt, c.readOnly, err) 45 } 46 t.Logf("data=%q", data) 47 for _, s := range c.expectedSubstrings { 48 if !strings.Contains(data, s) { 49 t.Fatalf("expected substring: %s, got %v (case=%+v)", s, data, c) 50 } 51 } 52 for _, s := range c.unexpectedSubstrings { 53 if strings.Contains(data, s) { 54 t.Fatalf("unexpected substring: %s, got %v (case=%+v)", s, data, c) 55 } 56 } 57 } 58 } 59 60 type mockFiProvider struct{} 61 62 func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) { 63 dirs := map[string]struct{}{ 64 `c:\`: {}, 65 `c:\windows\`: {}, 66 `c:\windows`: {}, 67 `c:\program files`: {}, 68 `c:\Windows`: {}, 69 `c:\Program Files (x86)`: {}, 70 `\\?\c:\windows\`: {}, 71 } 72 files := map[string]struct{}{ 73 `c:\windows\system32\ntdll.dll`: {}, 74 } 75 if _, ok := dirs[path]; ok { 76 return true, true, nil 77 } 78 if _, ok := files[path]; ok { 79 return true, false, nil 80 } 81 return false, false, nil 82 } 83 84 func TestParseMountRaw(t *testing.T) { 85 86 previousProvider := currentFileInfoProvider 87 defer func() { currentFileInfoProvider = previousProvider }() 88 currentFileInfoProvider = mockFiProvider{} 89 windowsSet := parseMountRawTestSet{ 90 valid: []string{ 91 `d:\`, 92 `d:`, 93 `d:\path`, 94 `d:\path with space`, 95 `c:\:d:\`, 96 `c:\windows\:d:`, 97 `c:\windows:d:\s p a c e`, 98 `c:\windows:d:\s p a c e:RW`, 99 `c:\program files:d:\s p a c e i n h o s t d i r`, 100 `0123456789name:d:`, 101 `MiXeDcAsEnAmE:d:`, 102 `name:D:`, 103 `name:D::rW`, 104 `name:D::RW`, 105 `name:D::RO`, 106 `c:/:d:/forward/slashes/are/good/too`, 107 `c:/:d:/including with/spaces:ro`, 108 `c:\Windows`, // With capital 109 `c:\Program Files (x86)`, // With capitals and brackets 110 `\\?\c:\windows\:d:`, // Long path handling (source) 111 `c:\windows\:\\?\d:\`, // Long path handling (target) 112 `\\.\pipe\foo:\\.\pipe\foo`, // named pipe 113 `//./pipe/foo://./pipe/foo`, // named pipe forward slashes 114 }, 115 invalid: map[string]string{ 116 ``: "invalid volume specification: ", 117 `.`: "invalid volume specification: ", 118 `..\`: "invalid volume specification: ", 119 `c:\:..\`: "invalid volume specification: ", 120 `c:\:d:\:xyzzy`: "invalid volume specification: ", 121 `c:`: "cannot be `c:`", 122 `c:\`: "cannot be `c:`", 123 `c:\notexist:d:`: `source path does not exist`, 124 `c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`, 125 `name<:d:`: `invalid volume specification`, 126 `name>:d:`: `invalid volume specification`, 127 `name::d:`: `invalid volume specification`, 128 `name":d:`: `invalid volume specification`, 129 `name\:d:`: `invalid volume specification`, 130 `name*:d:`: `invalid volume specification`, 131 `name|:d:`: `invalid volume specification`, 132 `name?:d:`: `invalid volume specification`, 133 `name/:d:`: `invalid volume specification`, 134 `d:\pathandmode:rw`: `invalid volume specification`, 135 `d:\pathandmode:ro`: `invalid volume specification`, 136 `con:d:`: `cannot be a reserved word for Windows filenames`, 137 `PRN:d:`: `cannot be a reserved word for Windows filenames`, 138 `aUx:d:`: `cannot be a reserved word for Windows filenames`, 139 `nul:d:`: `cannot be a reserved word for Windows filenames`, 140 `com1:d:`: `cannot be a reserved word for Windows filenames`, 141 `com2:d:`: `cannot be a reserved word for Windows filenames`, 142 `com3:d:`: `cannot be a reserved word for Windows filenames`, 143 `com4:d:`: `cannot be a reserved word for Windows filenames`, 144 `com5:d:`: `cannot be a reserved word for Windows filenames`, 145 `com6:d:`: `cannot be a reserved word for Windows filenames`, 146 `com7:d:`: `cannot be a reserved word for Windows filenames`, 147 `com8:d:`: `cannot be a reserved word for Windows filenames`, 148 `com9:d:`: `cannot be a reserved word for Windows filenames`, 149 `lpt1:d:`: `cannot be a reserved word for Windows filenames`, 150 `lpt2:d:`: `cannot be a reserved word for Windows filenames`, 151 `lpt3:d:`: `cannot be a reserved word for Windows filenames`, 152 `lpt4:d:`: `cannot be a reserved word for Windows filenames`, 153 `lpt5:d:`: `cannot be a reserved word for Windows filenames`, 154 `lpt6:d:`: `cannot be a reserved word for Windows filenames`, 155 `lpt7:d:`: `cannot be a reserved word for Windows filenames`, 156 `lpt8:d:`: `cannot be a reserved word for Windows filenames`, 157 `lpt9:d:`: `cannot be a reserved word for Windows filenames`, 158 `c:\windows\system32\ntdll.dll`: `Only directories can be mapped on this platform`, 159 `\\.\pipe\foo:c:\pipe`: `'c:\pipe' is not a valid pipe path`, 160 }, 161 } 162 lcowSet := parseMountRawTestSet{ 163 valid: []string{ 164 `/foo`, 165 `/foo/`, 166 `/foo bar`, 167 `c:\:/foo`, 168 `c:\windows\:/foo`, 169 `c:\windows:/s p a c e`, 170 `c:\windows:/s p a c e:RW`, 171 `c:\program files:/s p a c e i n h o s t d i r`, 172 `0123456789name:/foo`, 173 `MiXeDcAsEnAmE:/foo`, 174 `name:/foo`, 175 `name:/foo:rW`, 176 `name:/foo:RW`, 177 `name:/foo:RO`, 178 `c:/:/forward/slashes/are/good/too`, 179 `c:/:/including with/spaces:ro`, 180 `/Program Files (x86)`, // With capitals and brackets 181 }, 182 invalid: map[string]string{ 183 ``: "invalid volume specification: ", 184 `.`: "invalid volume specification: ", 185 `c:`: "invalid volume specification: ", 186 `c:\`: "invalid volume specification: ", 187 `../`: "invalid volume specification: ", 188 `c:\:../`: "invalid volume specification: ", 189 `c:\:/foo:xyzzy`: "invalid volume specification: ", 190 `/`: "destination can't be '/'", 191 `/..`: "destination can't be '/'", 192 `c:\notexist:/foo`: `source path does not exist`, 193 `c:\windows\system32\ntdll.dll:/foo`: `source path must be a directory`, 194 `name<:/foo`: `invalid volume specification`, 195 `name>:/foo`: `invalid volume specification`, 196 `name::/foo`: `invalid volume specification`, 197 `name":/foo`: `invalid volume specification`, 198 `name\:/foo`: `invalid volume specification`, 199 `name*:/foo`: `invalid volume specification`, 200 `name|:/foo`: `invalid volume specification`, 201 `name?:/foo`: `invalid volume specification`, 202 `name/:/foo`: `invalid volume specification`, 203 `/foo:rw`: `invalid volume specification`, 204 `/foo:ro`: `invalid volume specification`, 205 `con:/foo`: `cannot be a reserved word for Windows filenames`, 206 `PRN:/foo`: `cannot be a reserved word for Windows filenames`, 207 `aUx:/foo`: `cannot be a reserved word for Windows filenames`, 208 `nul:/foo`: `cannot be a reserved word for Windows filenames`, 209 `com1:/foo`: `cannot be a reserved word for Windows filenames`, 210 `com2:/foo`: `cannot be a reserved word for Windows filenames`, 211 `com3:/foo`: `cannot be a reserved word for Windows filenames`, 212 `com4:/foo`: `cannot be a reserved word for Windows filenames`, 213 `com5:/foo`: `cannot be a reserved word for Windows filenames`, 214 `com6:/foo`: `cannot be a reserved word for Windows filenames`, 215 `com7:/foo`: `cannot be a reserved word for Windows filenames`, 216 `com8:/foo`: `cannot be a reserved word for Windows filenames`, 217 `com9:/foo`: `cannot be a reserved word for Windows filenames`, 218 `lpt1:/foo`: `cannot be a reserved word for Windows filenames`, 219 `lpt2:/foo`: `cannot be a reserved word for Windows filenames`, 220 `lpt3:/foo`: `cannot be a reserved word for Windows filenames`, 221 `lpt4:/foo`: `cannot be a reserved word for Windows filenames`, 222 `lpt5:/foo`: `cannot be a reserved word for Windows filenames`, 223 `lpt6:/foo`: `cannot be a reserved word for Windows filenames`, 224 `lpt7:/foo`: `cannot be a reserved word for Windows filenames`, 225 `lpt8:/foo`: `cannot be a reserved word for Windows filenames`, 226 `lpt9:/foo`: `cannot be a reserved word for Windows filenames`, 227 `\\.\pipe\foo:/foo`: `Linux containers on Windows do not support named pipe mounts`, 228 }, 229 } 230 linuxSet := parseMountRawTestSet{ 231 valid: []string{ 232 "/home", 233 "/home:/home", 234 "/home:/something/else", 235 "/with space", 236 "/home:/with space", 237 "relative:/absolute-path", 238 "hostPath:/containerPath:ro", 239 "/hostPath:/containerPath:rw", 240 "/rw:/ro", 241 "/hostPath:/containerPath:shared", 242 "/hostPath:/containerPath:rshared", 243 "/hostPath:/containerPath:slave", 244 "/hostPath:/containerPath:rslave", 245 "/hostPath:/containerPath:private", 246 "/hostPath:/containerPath:rprivate", 247 "/hostPath:/containerPath:ro,shared", 248 "/hostPath:/containerPath:ro,slave", 249 "/hostPath:/containerPath:ro,private", 250 "/hostPath:/containerPath:ro,z,shared", 251 "/hostPath:/containerPath:ro,Z,slave", 252 "/hostPath:/containerPath:Z,ro,slave", 253 "/hostPath:/containerPath:slave,Z,ro", 254 "/hostPath:/containerPath:Z,slave,ro", 255 "/hostPath:/containerPath:slave,ro,Z", 256 "/hostPath:/containerPath:rslave,ro,Z", 257 "/hostPath:/containerPath:ro,rshared,Z", 258 "/hostPath:/containerPath:ro,Z,rprivate", 259 }, 260 invalid: map[string]string{ 261 "": "invalid volume specification", 262 "./": "mount path must be absolute", 263 "../": "mount path must be absolute", 264 "/:../": "mount path must be absolute", 265 "/:path": "mount path must be absolute", 266 ":": "invalid volume specification", 267 "/tmp:": "invalid volume specification", 268 ":test": "invalid volume specification", 269 ":/test": "invalid volume specification", 270 "tmp:": "invalid volume specification", 271 ":test:": "invalid volume specification", 272 "::": "invalid volume specification", 273 ":::": "invalid volume specification", 274 "/tmp:::": "invalid volume specification", 275 ":/tmp::": "invalid volume specification", 276 "/path:rw": "invalid volume specification", 277 "/path:ro": "invalid volume specification", 278 "/rw:rw": "invalid volume specification", 279 "path:ro": "invalid volume specification", 280 "/path:/path:sw": `invalid mode`, 281 "/path:/path:rwz": `invalid mode`, 282 "/path:/path:ro,rshared,rslave": `invalid mode`, 283 "/path:/path:ro,z,rshared,rslave": `invalid mode`, 284 "/path:shared": "invalid volume specification", 285 "/path:slave": "invalid volume specification", 286 "/path:private": "invalid volume specification", 287 "name:/absolute-path:shared": "invalid volume specification", 288 "name:/absolute-path:rshared": "invalid volume specification", 289 "name:/absolute-path:slave": "invalid volume specification", 290 "name:/absolute-path:rslave": "invalid volume specification", 291 "name:/absolute-path:private": "invalid volume specification", 292 "name:/absolute-path:rprivate": "invalid volume specification", 293 }, 294 } 295 296 linParser := &linuxParser{} 297 winParser := &windowsParser{} 298 lcowParser := &lcowParser{} 299 tester := func(parser Parser, set parseMountRawTestSet) { 300 301 for _, path := range set.valid { 302 303 if _, err := parser.ParseMountRaw(path, "local"); err != nil { 304 t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err) 305 } 306 } 307 308 for path, expectedError := range set.invalid { 309 if mp, err := parser.ParseMountRaw(path, "local"); err == nil { 310 t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp) 311 } else { 312 if !strings.Contains(err.Error(), expectedError) { 313 t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) 314 } 315 } 316 } 317 } 318 tester(linParser, linuxSet) 319 tester(winParser, windowsSet) 320 tester(lcowParser, lcowSet) 321 322 } 323 324 // testParseMountRaw is a structure used by TestParseMountRawSplit for 325 // specifying test cases for the ParseMountRaw() function. 326 type testParseMountRaw struct { 327 bind string 328 driver string 329 expType mount.Type 330 expDest string 331 expSource string 332 expName string 333 expDriver string 334 expRW bool 335 fail bool 336 } 337 338 func TestParseMountRawSplit(t *testing.T) { 339 previousProvider := currentFileInfoProvider 340 defer func() { currentFileInfoProvider = previousProvider }() 341 currentFileInfoProvider = mockFiProvider{} 342 windowsCases := []testParseMountRaw{ 343 {`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false}, 344 {`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, 345 {`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false}, 346 {`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, 347 {`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true}, 348 {`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, 349 {`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, 350 {`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false}, 351 {`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, 352 {`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, 353 {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false}, 354 {`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, 355 {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, 356 } 357 lcowCases := []testParseMountRaw{ 358 {`c:\:/foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false}, 359 {`c:\:/foo:ro`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, false}, 360 {`c:\:/foo:rw`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false}, 361 {`c:\:/foo:foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, true}, 362 {`name:/foo:rw`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false}, 363 {`name:/foo`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false}, 364 {`name:/foo:ro`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", false, false}, 365 {`name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, 366 {`driver/name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, 367 {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, true}, 368 {`\\.\pipe\foo:/data`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, 369 {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, 370 } 371 linuxCases := []testParseMountRaw{ 372 {"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false}, 373 {"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false}, 374 {"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false}, 375 {"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true}, 376 {"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false}, 377 {"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false}, 378 {"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false}, 379 {"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false}, 380 {"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true}, 381 } 382 linParser := &linuxParser{} 383 winParser := &windowsParser{} 384 lcowParser := &lcowParser{} 385 tester := func(parser Parser, cases []testParseMountRaw) { 386 for i, c := range cases { 387 t.Logf("case %d", i) 388 m, err := parser.ParseMountRaw(c.bind, c.driver) 389 if c.fail { 390 if err == nil { 391 t.Errorf("Expected error, was nil, for spec %s\n", c.bind) 392 } 393 continue 394 } 395 396 if m == nil || err != nil { 397 t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err.Error()) 398 continue 399 } 400 401 if m.Destination != c.expDest { 402 t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind) 403 } 404 405 if m.Source != c.expSource { 406 t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind) 407 } 408 409 if m.Name != c.expName { 410 t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind) 411 } 412 413 if m.Driver != c.expDriver { 414 t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind) 415 } 416 417 if m.RW != c.expRW { 418 t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind) 419 } 420 if m.Type != c.expType { 421 t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind) 422 } 423 } 424 } 425 426 tester(linParser, linuxCases) 427 tester(winParser, windowsCases) 428 tester(lcowParser, lcowCases) 429 } 430 431 func TestParseMountSpec(t *testing.T) { 432 type c struct { 433 input mount.Mount 434 expected MountPoint 435 } 436 testDir, err := ioutil.TempDir("", "test-mount-config") 437 if err != nil { 438 t.Fatal(err) 439 } 440 defer os.RemoveAll(testDir) 441 parser := NewParser(runtime.GOOS) 442 cases := []c{ 443 {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}}, 444 {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()}}, 445 {mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}}, 446 {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath + string(os.PathSeparator), ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}}, 447 {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: parser.DefaultCopyMode()}}, 448 {mount.Mount{Type: mount.TypeVolume, Target: testDestinationPath + string(os.PathSeparator)}, MountPoint{Type: mount.TypeVolume, Destination: testDestinationPath, RW: true, CopyData: parser.DefaultCopyMode()}}, 449 } 450 451 for i, c := range cases { 452 t.Logf("case %d", i) 453 mp, err := parser.ParseMountSpec(c.input) 454 if err != nil { 455 t.Error(err) 456 } 457 458 if c.expected.Type != mp.Type { 459 t.Errorf("Expected mount types to match. Expected: '%s', Actual: '%s'", c.expected.Type, mp.Type) 460 } 461 if c.expected.Destination != mp.Destination { 462 t.Errorf("Expected mount destination to match. Expected: '%s', Actual: '%s'", c.expected.Destination, mp.Destination) 463 } 464 if c.expected.Source != mp.Source { 465 t.Errorf("Expected mount source to match. Expected: '%s', Actual: '%s'", c.expected.Source, mp.Source) 466 } 467 if c.expected.RW != mp.RW { 468 t.Errorf("Expected mount writable to match. Expected: '%v', Actual: '%v'", c.expected.RW, mp.RW) 469 } 470 if c.expected.Propagation != mp.Propagation { 471 t.Errorf("Expected mount propagation to match. Expected: '%v', Actual: '%s'", c.expected.Propagation, mp.Propagation) 472 } 473 if c.expected.Driver != mp.Driver { 474 t.Errorf("Expected mount driver to match. Expected: '%v', Actual: '%s'", c.expected.Driver, mp.Driver) 475 } 476 if c.expected.CopyData != mp.CopyData { 477 t.Errorf("Expected mount copy data to match. Expected: '%v', Actual: '%v'", c.expected.CopyData, mp.CopyData) 478 } 479 } 480 }