master
cpp 605 lines 29 KB
Raw
1 // Copyright (C) Microsoft Corporation. All rights reserved.
2
3 #include "precomp.h"
4 #include "windows/Common.h"
5 #include "WSLCExecutor.h"
6 #include "WSLCE2EHelpers.h"
7 #include "TestImageRegistry.h"
8
9 namespace WSLCE2ETests {
10 using namespace wsl::shared;
11
12 class WSLCE2EContainerCpTests
13 {
14 WSLC_TEST_CLASS(WSLCE2EContainerCpTests)
15
16 TEST_CLASS_SETUP(ClassSetup)
17 {
18 TestImageRegistry::Instance().EnsureLoaded(DebianImage);
19 return true;
20 }
21
22 TEST_CLASS_CLEANUP(ClassCleanup)
23 {
24 EnsureContainerDoesNotExist(WslcContainerName);
25 return true;
26 }
27
28 TEST_METHOD_SETUP(MethodSetup)
29 {
30 EnsureContainerDoesNotExist(WslcContainerName);
31 TarPath = std::filesystem::current_path() / L"wslc-cp-test.tar";
32 DeleteFileW(TarPath.c_str());
33 return true;
34 }
35
36 TEST_METHOD_CLEANUP(MethodCleanup)
37 {
38 EnsureContainerDoesNotExist(WslcContainerName);
39 DeleteFileW(TarPath.c_str());
40 return true;
41 }
42
43 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_HelpCommand)
44 {
45 auto result = RunWslc(L"container cp --help");
46 VERIFY_IS_TRUE(result.ExitCode.has_value());
47 VERIFY_ARE_EQUAL(0u, result.ExitCode.value());
48 VERIFY_IS_TRUE(result.Stdout.has_value());
49 VERIFY_IS_TRUE(result.Stdout->find(L"container cp") != std::wstring::npos);
50 VERIFY_IS_TRUE(result.Stdout->find(L"source") != std::wstring::npos);
51 VERIFY_IS_TRUE(result.Stdout->find(L"target") != std::wstring::npos);
52 VERIFY_IS_TRUE(result.Stderr.has_value());
53 VERIFY_ARE_EQUAL(L"", result.Stderr.value());
54 }
55
56 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingBothArgs)
57 {
58 const auto result = RunWslc(L"container cp");
59 VERIFY_IS_TRUE(result.ExitCode.has_value());
60 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
61 VERIFY_IS_TRUE(result.Stderr.has_value());
62 VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'source'") != std::wstring::npos);
63 }
64
65 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_MissingTarget)
66 {
67 const auto result = RunWslc(L"container cp -");
68 VERIFY_IS_TRUE(result.ExitCode.has_value());
69 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
70 VERIFY_IS_TRUE(result.Stderr.has_value());
71 VERIFY_IS_TRUE(result.Stderr->find(L"Required argument not provided: 'target'") != std::wstring::npos);
72 }
73
74 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_StdinIsTerminal)
75 {
76 // Running without piped stdin should fail with a terminal error.
77 // RunWslcAndRedirectToFile gives the child a real console stdout handle,
78 // and since RunWslc pipes NUL to stdin, we use RunWslcAndRedirectToFile
79 // with no output path to get a real console for the child.
80 const auto result = RunWslcAndRedirectToFile(L"container cp - fakecontainer:/path");
81 VERIFY_IS_TRUE(result.ExitCode.has_value());
82 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
83 VERIFY_IS_TRUE(result.Stderr.has_value());
84 VERIFY_IS_TRUE(result.Stderr->find(L"Cannot read tar data from terminal") != std::wstring::npos);
85 }
86
87 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_SourceNotStdin)
88 {
89 // A local file that doesn't exist should fail with a "source not found" error.
90 // Use RunWslc which pipes NUL to stdin (not a terminal).
91 const auto result = RunWslc(L"container cp somefile.tar fakecontainer:/path");
92 VERIFY_IS_TRUE(result.ExitCode.has_value());
93 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
94 VERIFY_IS_TRUE(result.Stderr.has_value());
95 VERIFY_IS_TRUE(result.Stderr->find(L"Source path not found: somefile.tar") != std::wstring::npos);
96 }
97
98 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_NoColon)
99 {
100 // Target must be CONTAINER:PATH — missing colon should fail.
101 const auto result = RunWslc(L"container cp - fakecontainer_nopath");
102 VERIFY_IS_TRUE(result.ExitCode.has_value());
103 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
104 VERIFY_IS_TRUE(result.Stderr.has_value());
105 VERIFY_IS_TRUE(result.Stderr->find(L"Invalid copy direction") != std::wstring::npos);
106 }
107
108 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyContainer)
109 {
110 // Target with empty container name (:path) should fail.
111 const auto result = RunWslc(L"container cp - :/path");
112 VERIFY_IS_TRUE(result.ExitCode.has_value());
113 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
114 VERIFY_IS_TRUE(result.Stderr.has_value());
115 VERIFY_IS_TRUE(result.Stderr->find(L"Invalid copy direction") != std::wstring::npos);
116 }
117
118 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidTargetFormat_EmptyPath)
119 {
120 // Target with empty path (container:) should fail.
121 const auto result = RunWslc(L"container cp - fakecontainer:");
122 VERIFY_IS_TRUE(result.ExitCode.has_value());
123 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
124 VERIFY_IS_TRUE(result.Stderr.has_value());
125 VERIFY_IS_TRUE(result.Stderr->find(L"Invalid destination format") != std::wstring::npos);
126 }
127
128 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerNotFound)
129 {
130 // Create a valid tar file to pipe in, but target a nonexistent container.
131 CreateTestTarFile();
132
133 const auto result = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", InvalidContainerName), TarPath);
134 VERIFY_IS_TRUE(result.ExitCode.has_value());
135 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
136 VERIFY_IS_TRUE(result.Stderr.has_value());
137 VERIFY_IS_TRUE(result.Stderr->find(L"not found") != std::wstring::npos);
138 }
139
140 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Success)
141 {
142 // Create and start a container with sleep infinity to keep it running.
143 auto runResult =
144 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
145 runResult.Verify({.Stderr = L"", .ExitCode = 0});
146
147 // Create a test tar file with a known file inside.
148 CreateTestTarFile();
149
150 // Cp the tar into the running container.
151 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath);
152 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
153
154 // Verify the file was copied by running a command inside the container.
155 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
156 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
157 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
158 VERIFY_IS_TRUE(execResult.Stdout.has_value());
159 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
160 }
161
162 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_StdinFromPipe)
163 {
164 // Validate that cp works when stdin is a pipe (no content-length available),
165 // as opposed to a file where GetFileSize can determine the length upfront.
166 auto runResult =
167 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
168 runResult.Verify({.Stderr = L"", .ExitCode = 0});
169
170 CreateTestTarFile();
171
172 // Open the tar file and relay its content into a pipe on a background thread.
173 wil::unique_hfile tarFile(
174 CreateFileW(TarPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
175 THROW_LAST_ERROR_IF(!tarFile);
176
177 auto [pipeRead, pipeWrite] = wsl::windows::common::wslutil::OpenAnonymousPipe(0, false, false);
178
179 std::thread relayThread([&tarFile, &pipeWrite] {
180 wsl::windows::common::relay::InterruptableRelay(tarFile.get(), pipeWrite.get());
181 pipeWrite.reset();
182 });
183
184 // Pass the pipe read end as stdin — wslc cannot determine content-length from a pipe.
185 const auto cpResult = RunWslc(std::format(L"container cp - {}:/tmp", WslcContainerName), ElevationType::Elevated, pipeRead.get());
186 relayThread.join();
187 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
188
189 // Verify the file was copied.
190 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
191 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
192 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
193 VERIFY_IS_TRUE(execResult.Stdout.has_value());
194 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
195 }
196
197 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ToStoppedContainer)
198 {
199 // Create a stopped container (not started).
200 auto createResult = RunWslc(std::format(L"container create --name {} {}", WslcContainerName, DebianImage.NameAndTag()));
201 createResult.Verify({.Stderr = L"", .ExitCode = 0});
202
203 // Create a test tar file.
204 CreateTestTarFile();
205
206 // Attempt to cp into the stopped container — Docker should accept this.
207 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp - {}:/tmp", WslcContainerName), TarPath);
208
209 // Docker's PUT /archive works on stopped containers too.
210 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
211 }
212
213 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlag)
214 {
215 // Create and start a container.
216 auto runResult =
217 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
218 runResult.Verify({.Stderr = L"", .ExitCode = 0});
219
220 CreateTestTarFile();
221
222 // Cp with -a flag (archive mode preserves uid/gid).
223 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a - {}:/tmp", WslcContainerName), TarPath);
224 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
225
226 // Verify the file was copied.
227 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
228 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
229 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
230 VERIFY_IS_TRUE(execResult.Stdout.has_value());
231 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
232 }
233
234 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagLongForm)
235 {
236 // Create and start a container.
237 auto runResult =
238 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
239 runResult.Verify({.Stderr = L"", .ExitCode = 0});
240
241 CreateTestTarFile();
242
243 // Cp with --archive flag (long form).
244 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive - {}:/tmp", WslcContainerName), TarPath);
245 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
246
247 // Verify the file was copied.
248 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
249 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
250 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
251 VERIFY_IS_TRUE(execResult.Stdout.has_value());
252 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
253 }
254
255 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsTrue)
256 {
257 // Test -a=true syntax.
258 auto runResult =
259 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
260 runResult.Verify({.Stderr = L"", .ExitCode = 0});
261
262 CreateTestTarFile();
263
264 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=true - {}:/tmp", WslcContainerName), TarPath);
265 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
266
267 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
268 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
269 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
270 VERIFY_IS_TRUE(execResult.Stdout.has_value());
271 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
272 }
273
274 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagEqualsFalse)
275 {
276 // Test -a=false syntax (no archive mode).
277 auto runResult =
278 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
279 runResult.Verify({.Stderr = L"", .ExitCode = 0});
280
281 CreateTestTarFile();
282
283 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp -a=false - {}:/tmp", WslcContainerName), TarPath);
284 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
285
286 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
287 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
288 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
289 VERIFY_IS_TRUE(execResult.Stdout.has_value());
290 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
291 }
292
293 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsTrue)
294 {
295 // Test --archive=true syntax.
296 auto runResult =
297 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
298 runResult.Verify({.Stderr = L"", .ExitCode = 0});
299
300 CreateTestTarFile();
301
302 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=true - {}:/tmp", WslcContainerName), TarPath);
303 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
304
305 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
306 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
307 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
308 VERIFY_IS_TRUE(execResult.Stdout.has_value());
309 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
310 }
311
312 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveLongFormEqualsFalse)
313 {
314 // Test --archive=false syntax (no archive mode).
315 auto runResult =
316 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
317 runResult.Verify({.Stderr = L"", .ExitCode = 0});
318
319 CreateTestTarFile();
320
321 const auto cpResult = RunWslcWithStdinFile(std::format(L"container cp --archive=false - {}:/tmp", WslcContainerName), TarPath);
322 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
323
324 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/testfile.txt", WslcContainerName));
325 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
326 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
327 VERIFY_IS_TRUE(execResult.Stdout.has_value());
328 VERIFY_ARE_EQUAL(L"wslc-cp-test-content\n", execResult.Stdout.value());
329 }
330
331 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ArchiveFlagInvalidValue)
332 {
333 // Test -a=invalid should fail with an error.
334 CreateTestTarFile();
335
336 const auto result = RunWslcWithStdinFile(std::format(L"container cp -a=invalid - {}:/tmp", WslcContainerName), TarPath);
337 VERIFY_IS_TRUE(result.ExitCode.has_value());
338 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
339 VERIFY_IS_TRUE(result.Stderr.has_value());
340 VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
341 }
342
343 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileToContainer)
344 {
345 // Create and start a container.
346 auto runResult =
347 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
348 runResult.Verify({.Stderr = L"", .ExitCode = 0});
349
350 // Create a local file to copy.
351 auto localFile = std::filesystem::current_path() / L"wslc-cp-local-test.txt";
352 auto cleanupLocal = wil::scope_exit([&] { DeleteFileW(localFile.c_str()); });
353
354 {
355 wil::unique_hfile file(CreateFileW(localFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
356 THROW_LAST_ERROR_IF(!file);
357 const std::string content = "local-file-content\n";
358 DWORD written = 0;
359 THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast<DWORD>(content.size()), &written, nullptr));
360 }
361
362 // Copy local file to container.
363 const auto cpResult = RunWslc(std::format(L"container cp {} {}:/tmp/", localFile.wstring(), WslcContainerName));
364 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
365
366 // Verify the file was copied.
367 const auto execResult = RunWslc(std::format(L"container exec {} cat /tmp/wslc-cp-local-test.txt", WslcContainerName));
368 VERIFY_IS_TRUE(execResult.ExitCode.has_value());
369 VERIFY_ARE_EQUAL(0u, execResult.ExitCode.value());
370 VERIFY_IS_TRUE(execResult.Stdout.has_value());
371 VERIFY_ARE_EQUAL(L"local-file-content\n", execResult.Stdout.value());
372 }
373
374 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_LocalFileNotFound)
375 {
376 // Copying a nonexistent local file should fail.
377 const auto result = RunWslc(std::format(L"container cp C:\\nonexistent_wslc_test_file.txt {}:/tmp/", WslcContainerName));
378 VERIFY_IS_TRUE(result.ExitCode.has_value());
379 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
380 VERIFY_IS_TRUE(result.Stderr.has_value());
381 VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
382 }
383
384 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal)
385 {
386 // Create and start a container.
387 auto runResult =
388 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
389 runResult.Verify({.Stderr = L"", .ExitCode = 0});
390
391 // Create a file inside the container using exec.
392 auto execResult =
393 RunWslc(std::format(L"container exec {} sh -c \"echo container-content > /tmp/fromcontainer.txt\"", WslcContainerName));
394 execResult.Verify({.ExitCode = 0});
395
396 // Create a directory to download into.
397 auto downloadDir = std::filesystem::current_path() / L"wslc-cp-download-test";
398 std::filesystem::create_directories(downloadDir);
399 auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
400
401 // Copy from container to local.
402 const auto cpResult =
403 RunWslc(std::format(L"container cp {}:/tmp/fromcontainer.txt {}", WslcContainerName, downloadDir.wstring()));
404 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
405
406 // Verify the file was extracted locally.
407 auto extractedFile = downloadDir / L"fromcontainer.txt";
408 VERIFY_IS_TRUE(std::filesystem::exists(extractedFile));
409 }
410
411 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_TrailingBackslash)
412 {
413 // Regression test: trailing backslash on local path should not break tar extraction.
414 auto runResult =
415 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
416 runResult.Verify({.Stderr = L"", .ExitCode = 0});
417
418 // Create the file inside the container using exec.
419 auto execResult = RunWslc(std::format(L"container exec {} sh -c \"echo backslash-test > /tmp/bstest.txt\"", WslcContainerName));
420 execResult.Verify({.ExitCode = 0});
421
422 auto downloadDir = std::filesystem::current_path() / L"wslc-cp-backslash-test";
423 std::filesystem::create_directories(downloadDir);
424 auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
425
426 // Copy with explicit trailing backslash in target path.
427 auto targetWithBackslash = downloadDir.wstring() + L"\\";
428 const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/bstest.txt {}", WslcContainerName, targetWithBackslash));
429 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
430
431 auto extractedFile = downloadDir / L"bstest.txt";
432 VERIFY_IS_TRUE(std::filesystem::exists(extractedFile));
433 }
434
435 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_ThroughSymlink)
436 {
437 auto runResult =
438 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
439 runResult.Verify({.Stderr = L"", .ExitCode = 0});
440
441 auto execResult = RunWslc(std::format(L"container exec {} touch /tmp/symfile.txt", WslcContainerName));
442 execResult.Verify({.ExitCode = 0});
443
444 auto linkDir = std::filesystem::current_path() / L"wslc-cp-symlink-link";
445 auto cleanup = wil::scope_exit([&] {
446 std::error_code ec;
447 std::filesystem::remove(linkDir, ec);
448 std::filesystem::remove(L"symfile.txt", ec);
449 });
450
451 // Creating a symlink requires Administrator privileges, which the E2E tests already run with.
452 THROW_LAST_ERROR_IF(!CreateSymbolicLinkW(linkDir.c_str(), std::filesystem::current_path().c_str(), SYMBOLIC_LINK_FLAG_DIRECTORY));
453
454 // Copy from the container into the symlink.
455 const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/symfile.txt {}", WslcContainerName, linkDir.wstring()));
456 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
457
458 // The file must land in the real directory (and therefore be visible through the symlink).
459 VERIFY_IS_TRUE(std::filesystem::exists(std::filesystem::current_path() / L"symfile.txt"));
460 VERIFY_IS_TRUE(std::filesystem::exists(linkDir / L"symfile.txt"));
461 }
462
463 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_FileDestination)
464 {
465 // When the local target doesn't end with a separator and isn't an existing directory,
466 // it should be treated as a file destination (matching docker cp semantics).
467 auto runResult =
468 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
469 runResult.Verify({.Stderr = L"", .ExitCode = 0});
470
471 // Create the file inside the container using exec.
472 auto execResult = RunWslc(std::format(L"container exec {} sh -c \"echo file-dest-test > /tmp/srcfile.txt\"", WslcContainerName));
473 execResult.Verify({.ExitCode = 0});
474
475 auto targetFile = std::filesystem::current_path() / L"wslc-cp-file-dest-test" / L"renamed.txt";
476 auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(targetFile.parent_path()); });
477
478 // Copy from container to a specific file path (not a directory).
479 const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/srcfile.txt {}", WslcContainerName, targetFile.wstring()));
480 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
481
482 // The file should exist at the exact target path, not inside a directory named "renamed.txt".
483 VERIFY_IS_TRUE(std::filesystem::exists(targetFile));
484 VERIFY_IS_TRUE(std::filesystem::is_regular_file(targetFile));
485 }
486
487 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentPath)
488 {
489 // Regression test: DownloadArchive used to hang on 404 because the HTTP/1.1 keep-alive
490 // socket never closed. The fix shuts down the socket so the read sees EOF immediately.
491 auto runResult =
492 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
493 runResult.Verify({.Stderr = L"", .ExitCode = 0});
494
495 auto downloadDir = std::filesystem::current_path() / L"wslc-cp-notfound-test";
496 std::filesystem::create_directories(downloadDir);
497 auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
498
499 const auto cpResult = RunWslc(std::format(L"container cp {}:/nonexistent/file.txt {}", WslcContainerName, downloadDir.wstring()));
500 VERIFY_IS_TRUE(cpResult.ExitCode.has_value());
501 VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value());
502 VERIFY_IS_TRUE(cpResult.Stderr.has_value());
503 VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size());
504 }
505
506 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_ContainerToLocal_NonexistentDir)
507 {
508 // Regression test: DownloadArchive 404 for a nonexistent directory path.
509 auto runResult =
510 RunWslc(std::format(L"container run -d --name {} {} sleep infinity", WslcContainerName, DebianImage.NameAndTag()));
511 runResult.Verify({.Stderr = L"", .ExitCode = 0});
512
513 auto downloadDir = std::filesystem::current_path() / L"wslc-cp-notfound-dir-test";
514 std::filesystem::create_directories(downloadDir);
515 auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
516
517 const auto cpResult = RunWslc(std::format(L"container cp {}:/no/such/directory/ {}", WslcContainerName, downloadDir.wstring()));
518 VERIFY_IS_TRUE(cpResult.ExitCode.has_value());
519 VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value());
520 VERIFY_IS_TRUE(cpResult.Stderr.has_value());
521 VERIFY_ARE_NOT_EQUAL(0u, cpResult.Stderr.value().size());
522 }
523
524 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_Download_NonexistentContainer)
525 {
526 // Regression test: DownloadArchive error path when the container itself doesn't exist.
527 auto downloadDir = std::filesystem::current_path() / L"wslc-cp-no-container-test";
528 std::filesystem::create_directories(downloadDir);
529 auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
530
531 const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/file.txt {}", InvalidContainerName, downloadDir.wstring()));
532 VERIFY_IS_TRUE(cpResult.ExitCode.has_value());
533 VERIFY_ARE_EQUAL(1u, cpResult.ExitCode.value());
534 VERIFY_IS_TRUE(cpResult.Stderr.has_value());
535 VERIFY_IS_TRUE(cpResult.Stderr->find(L"not found") != std::wstring::npos);
536 }
537
538 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_FromStoppedContainer)
539 {
540 // Create a container, put a file in it, stop it, then copy out.
541 auto runResult = RunWslc(std::format(
542 L"container run --name {} {} sh -c \"echo stopped-content > /tmp/stopped.txt\"", WslcContainerName, DebianImage.NameAndTag()));
543 runResult.Verify({.Stderr = L"", .ExitCode = 0});
544
545 // Container has exited (ran a one-shot command). Copy from the stopped container.
546 auto downloadDir = std::filesystem::current_path() / L"wslc-cp-stopped-test";
547 std::filesystem::create_directories(downloadDir);
548 auto cleanupDir = wil::scope_exit([&] { std::filesystem::remove_all(downloadDir); });
549
550 const auto cpResult = RunWslc(std::format(L"container cp {}:/tmp/stopped.txt {}", WslcContainerName, downloadDir.wstring()));
551 cpResult.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
552
553 auto extractedFile = downloadDir / L"stopped.txt";
554 VERIFY_IS_TRUE(std::filesystem::exists(extractedFile));
555 }
556
557 WSLC_TEST_METHOD(WSLCE2E_Container_Cp_InvalidDirection_LocalToLocal)
558 {
559 // local → local is not a valid copy direction.
560 const auto result = RunWslc(L"container cp C:\\temp\\somefile.txt C:\\temp\\dest\\");
561 VERIFY_IS_TRUE(result.ExitCode.has_value());
562 VERIFY_ARE_EQUAL(1u, result.ExitCode.value());
563 VERIFY_IS_TRUE(result.Stderr.has_value());
564 VERIFY_ARE_NOT_EQUAL(0u, result.Stderr.value().size());
565 }
566
567 private:
568 const std::wstring WslcContainerName = L"wslc-test-container-cp";
569 const std::wstring InvalidContainerName = L"wslc-nonexistent-container-for-cp";
570 const TestImage& DebianImage = DebianTestImage();
571
572 std::filesystem::path TarPath{};
573
574 // Creates a tar file containing a single text file using tar.exe.
575 void CreateTestTarFile()
576 {
577 // Create a directory with a test file to archive.
578 auto tarSrcDir = std::filesystem::current_path() / L"wslc-cp-tar-src";
579 std::filesystem::create_directories(tarSrcDir);
580 auto cleanupSrcDir = wil::scope_exit([&] { std::filesystem::remove_all(tarSrcDir); });
581
582 auto testFile = tarSrcDir / L"testfile.txt";
583 {
584 wil::unique_hfile file(CreateFileW(testFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
585 THROW_LAST_ERROR_IF(!file);
586 const std::string content = "wslc-cp-test-content\n";
587 DWORD written = 0;
588 THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), content.data(), static_cast<DWORD>(content.size()), &written, nullptr));
589 }
590
591 // Use tar.exe to create the archive.
592 auto tarCmd = std::format(L"tar.exe -cf \"{}\" -C \"{}\" testfile.txt", TarPath.wstring(), tarSrcDir.wstring());
593 STARTUPINFOW si{sizeof(si)};
594 PROCESS_INFORMATION pi{};
595 THROW_LAST_ERROR_IF(!CreateProcessW(nullptr, tarCmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi));
596 wil::unique_handle tarProcess(pi.hProcess);
597 wil::unique_handle tarThread(pi.hThread);
598 WaitForSingleObject(tarProcess.get(), INFINITE);
599
600 DWORD exitCode = 0;
601 GetExitCodeProcess(tarProcess.get(), &exitCode);
602 THROW_HR_IF_MSG(E_FAIL, exitCode != 0, "tar.exe exited with code %u", exitCode);
603 }
604 };
605 } // namespace WSLCE2ETests