1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using Microsoft.Win32.SafeHandles;
6 using System.Collections.Generic;
7 using System.IO.Pipes;
8 using System.Runtime.InteropServices;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Xunit;
12 
13 namespace System.IO.Tests
14 {
15     public class FileStream_CopyToAsync : FileSystemTest
16     {
17         [Theory]
18         [InlineData(false)]
19         [InlineData(true)]
InvalidArgs_Throws(bool useAsync)20         public void InvalidArgs_Throws(bool useAsync)
21         {
22             using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
23             {
24                 AssertExtensions.Throws<ArgumentNullException>("destination", () => { fs.CopyToAsync(null); });
25                 AssertExtensions.Throws<ArgumentOutOfRangeException>("bufferSize", () => { fs.CopyToAsync(new MemoryStream(), 0); });
26                 Assert.Throws<NotSupportedException>(() => { fs.CopyToAsync(new MemoryStream(new byte[1], writable: false)); });
27                 fs.Dispose();
28                 Assert.Throws<ObjectDisposedException>(() => { fs.CopyToAsync(new MemoryStream()); });
29             }
30             using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.Write))
31             {
32                 Assert.Throws<NotSupportedException>(() => { fs.CopyToAsync(new MemoryStream()); });
33             }
34             using (FileStream src = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
35             using (FileStream dst = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
36             {
37                 dst.Dispose();
38                 Assert.Throws<ObjectDisposedException>(() => { src.CopyToAsync(dst); });
39             }
40         }
41 
42         [Theory]
43         [InlineData(false)]
44         [InlineData(true)]
45         [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "The Stream CopyToAsync fails on netcoreapp because it calls Length which checks the validity of the underlying handle. On NetFX the operation no-ops for no input or delays failure to execution for input. See /dotnet/coreclr/pull/4540.")]
DisposeHandleThenUseFileStream_CopyToAsync(bool useAsync)46         public void DisposeHandleThenUseFileStream_CopyToAsync(bool useAsync)
47         {
48             using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
49             {
50                 fs.SafeFileHandle.Dispose();
51                 Assert.Throws<ObjectDisposedException>(() => { fs.CopyToAsync(new MemoryStream()); });
52             }
53 
54             using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
55             {
56                 fs.Write(TestBuffer, 0, TestBuffer.Length);
57                 fs.SafeFileHandle.Dispose();
58                 Assert.Throws<ObjectDisposedException>(() => { fs.CopyToAsync(new MemoryStream()).Wait(); });
59             }
60         }
61 
62         [Theory]
63         [InlineData(false)]
64         [InlineData(true)]
65         [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework, "The Stream CopyToAsync fails on netcoreapp because it calls Length which checks the validity of the underlying handle. On NetFX the operation no-ops for no input or delays failure to execution for input. See /dotnet/coreclr/pull/4540.")]
DisposeHandleThenUseFileStream_CopyToAsync_netfx(bool useAsync)66         public void DisposeHandleThenUseFileStream_CopyToAsync_netfx(bool useAsync)
67         {
68             using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
69             {
70                 fs.SafeFileHandle.Dispose();
71                 fs.CopyToAsync(new MemoryStream());
72             }
73 
74             using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
75             {
76                 fs.Write(TestBuffer, 0, TestBuffer.Length);
77                 fs.SafeFileHandle.Dispose();
78                 try
79                 {
80                     fs.CopyToAsync(new MemoryStream()).Wait();
81                 }
82                 catch (AggregateException e)
83                 {
84                     Assert.Equal(typeof(ObjectDisposedException), e.InnerException.GetType());
85                 }
86             }
87         }
88 
89         [Theory]
90         [InlineData(false)]
91         [InlineData(true)]
AlreadyCanceled_ReturnsCanceledTask(bool useAsync)92         public async Task AlreadyCanceled_ReturnsCanceledTask(bool useAsync)
93         {
94             using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync))
95             {
96                 await Assert.ThrowsAnyAsync<OperationCanceledException>(() => fs.CopyToAsync(fs, 0x1000, new CancellationToken(canceled: true)));
97             }
98         }
99 
100         [Theory] // inner loop, just a few cases
101         [InlineData(false, false)]
102         [InlineData(true, false)]
103         [InlineData(true, true)]
File_AllDataCopied_InnerLoop(bool useAsync, bool preWrite)104         public Task File_AllDataCopied_InnerLoop(bool useAsync, bool preWrite)
105         {
106             return File_AllDataCopied(
107                 _ => new MemoryStream(), useAsync, preRead: false, preWrite: preWrite, exposeHandle: false, cancelable: true,
108                 bufferSize: 4096, writeSize: 1024, numWrites: 10);
109         }
110 
111         [Theory] // outer loop, many combinations
112         [OuterLoop]
113         [MemberData(nameof(File_AllDataCopied_MemberData))]
File_AllDataCopied( Func<string, Stream> createDestinationStream, bool useAsync, bool preRead, bool preWrite, bool exposeHandle, bool cancelable, int bufferSize, int writeSize, int numWrites)114         public async Task File_AllDataCopied(
115             Func<string, Stream> createDestinationStream,
116             bool useAsync, bool preRead, bool preWrite, bool exposeHandle, bool cancelable,
117             int bufferSize, int writeSize, int numWrites)
118         {
119             // Create the expected data
120             long totalLength = writeSize * numWrites;
121             var expectedData = new byte[totalLength];
122             new Random(42).NextBytes(expectedData);
123 
124             // Write it out into the source file
125             string srcPath = GetTestFilePath();
126             File.WriteAllBytes(srcPath, expectedData);
127 
128             string dstPath = GetTestFilePath();
129             using (FileStream src = new FileStream(srcPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, bufferSize, useAsync))
130             using (Stream dst = createDestinationStream(dstPath))
131             {
132                 // If configured to expose the handle, do so.  This influences the stream's need to ensure the position is in sync.
133                 if (exposeHandle)
134                 {
135                     var ignored = src.SafeFileHandle;
136                 }
137 
138                 // If configured to "preWrite", do a write before we start reading.
139                 if (preWrite)
140                 {
141                     src.Write(new byte[] { 42 }, 0, 1);
142                     dst.Write(new byte[] { 42 }, 0, 1);
143                     expectedData[0] = 42;
144                 }
145 
146                 // If configured to "preRead", read one byte from the source prior to the CopyToAsync.
147                 // This helps test what happens when there's already data in the buffer, when the position
148                 // isn't starting at zero, etc.
149                 if (preRead)
150                 {
151                     int initialByte = src.ReadByte();
152                     if (initialByte >= 0)
153                     {
154                         dst.WriteByte((byte)initialByte);
155                     }
156                 }
157 
158                 // Do the copy
159                 await src.CopyToAsync(dst, writeSize, cancelable ? new CancellationTokenSource().Token : CancellationToken.None);
160                 dst.Flush();
161 
162                 // Make sure we're at the end of the source file
163                 Assert.Equal(src.Length, src.Position);
164 
165                 // Verify the copied data
166                 dst.Position = 0;
167                 var result = new MemoryStream();
168                 dst.CopyTo(result);
169                 byte[] actualData = result.ToArray();
170                 Assert.Equal(expectedData.Length, actualData.Length);
171                 Assert.Equal<byte>(expectedData, actualData);
172             }
173         }
174 
File_AllDataCopied_MemberData()175         public static IEnumerable<object[]> File_AllDataCopied_MemberData()
176         {
177             bool[] bools = new[] { true, false };
178             foreach (bool useAsync in bools) // sync or async mode
179             {
180                 foreach (bool preRead in bools) // whether to do a read before the CopyToAsync
181                 {
182                     foreach (bool cancelable in bools) // whether to use a cancelable token
183                     {
184                         for (int streamType = 0; streamType < 2; streamType++) // kind of stream to use
185                         {
186                             Func<string, Stream> createDestinationStream;
187                             switch (streamType)
188                             {
189                                 case 0: createDestinationStream = _ => new MemoryStream(); break;
190                                 default: createDestinationStream = s => File.Create(s); break;
191                             }
192 
193                             // Various exposeHandle (whether the SafeFileHandle was publicly accessed),
194                             // preWrite, bufferSize, writeSize, and numWrites combinations
195                             yield return new object[] { createDestinationStream, useAsync, preRead, false, false, cancelable, 0x1000, 0x100, 100 };
196                             yield return new object[] { createDestinationStream, useAsync, preRead, false, false, cancelable, 0x1, 0x1, 1000 };
197                             yield return new object[] { createDestinationStream, useAsync, preRead, false, true, cancelable, 0x2, 0x100, 100 };
198                             yield return new object[] { createDestinationStream, useAsync, preRead, false, false, cancelable, 0x4000, 0x10, 100 };
199                             yield return new object[] { createDestinationStream, useAsync, preRead, false, true, cancelable, 0x1000, 99999, 10 };
200                         }
201                     }
202                 }
203             }
204         }
205 
206         [Theory]
207         [InlineData(10, 1024)]
AnonymousPipeViaFileStream_AllDataCopied(int writeSize, int numWrites)208         public async Task AnonymousPipeViaFileStream_AllDataCopied(int writeSize, int numWrites)
209         {
210             long totalLength = writeSize * numWrites;
211             var expectedData = new byte[totalLength];
212             new Random(42).NextBytes(expectedData);
213 
214             var results = new MemoryStream();
215 
216             using (var server = new AnonymousPipeServerStream(PipeDirection.Out))
217             {
218                 Task serverTask = Task.Run(async () =>
219                 {
220                     for (int i = 0; i < numWrites; i++)
221                     {
222                         await server.WriteAsync(expectedData, i * writeSize, writeSize);
223                     }
224                 });
225 
226                 using (var client = new FileStream(new SafeFileHandle(server.ClientSafePipeHandle.DangerousGetHandle(), false), FileAccess.Read, bufferSize: 3))
227                 {
228                     Task copyTask = client.CopyToAsync(results, writeSize);
229                     await await Task.WhenAny(serverTask, copyTask);
230 
231                     server.Dispose();
232                     await copyTask;
233                 }
234             }
235 
236             byte[] actualData = results.ToArray();
237             Assert.Equal(expectedData.Length, actualData.Length);
238             Assert.Equal<byte>(expectedData, actualData);
239         }
240 
241         [PlatformSpecific(TestPlatforms.Windows)] // Uses P/Invokes to create async pipe handle
242         [Theory]
243         [InlineData(false, 10, 1024)]
244         [InlineData(true, 10, 1024)]
245         [ActiveIssue(22271, TargetFrameworkMonikers.UapNotUapAot)]
NamedPipeViaFileStream_AllDataCopied(bool useAsync, int writeSize, int numWrites)246         public async Task NamedPipeViaFileStream_AllDataCopied(bool useAsync, int writeSize, int numWrites)
247         {
248             long totalLength = writeSize * numWrites;
249             var expectedData = new byte[totalLength];
250             new Random(42).NextBytes(expectedData);
251 
252             var results = new MemoryStream();
253             var pipeOptions = useAsync ? PipeOptions.Asynchronous : PipeOptions.None;
254 
255             string name = GetNamedPipeServerStreamName();
256             using (var server = new NamedPipeServerStream(name, PipeDirection.Out, 1, PipeTransmissionMode.Byte, pipeOptions))
257             {
258                 Task serverTask = Task.Run(async () =>
259                 {
260                     await server.WaitForConnectionAsync();
261                     for (int i = 0; i < numWrites; i++)
262                     {
263                         await server.WriteAsync(expectedData, i * writeSize, writeSize);
264                     }
265                     server.Dispose();
266                 });
267 
268                 Assert.True(WaitNamedPipeW(@"\\.\pipe\" + name, -1));
269                 using (SafeFileHandle clientHandle = CreateFileW(@"\\.\pipe\" + name, GENERIC_READ, FileShare.None, IntPtr.Zero, FileMode.Open, (int)pipeOptions, IntPtr.Zero))
270                 using (var client = new FileStream(clientHandle, FileAccess.Read, bufferSize: 3, isAsync: useAsync))
271                 {
272                     Task copyTask = client.CopyToAsync(results, (int)totalLength);
273                     await await Task.WhenAny(serverTask, copyTask);
274                     await copyTask;
275                 }
276             }
277 
278             byte[] actualData = results.ToArray();
279             Assert.Equal(expectedData.Length, actualData.Length);
280             Assert.Equal<byte>(expectedData, actualData);
281         }
282 
283         [PlatformSpecific(TestPlatforms.Windows)] // Uses P/Invokes to create async pipe handle
284         [Theory]
NamedPipeViaFileStream_CancellationRequested_OperationCanceled()285         public async Task NamedPipeViaFileStream_CancellationRequested_OperationCanceled()
286         {
287             string name = Guid.NewGuid().ToString("N");
288             using (var server = new NamedPipeServerStream(name, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous))
289             {
290                 Task serverTask = server.WaitForConnectionAsync();
291 
292                 Assert.True(WaitNamedPipeW(@"\\.\pipe\" + name, -1));
293                 using (SafeFileHandle clientHandle = CreateFileW(@"\\.\pipe\" + name, GENERIC_READ, FileShare.None, IntPtr.Zero, FileMode.Open, (int)PipeOptions.Asynchronous, IntPtr.Zero))
294                 using (var client = new FileStream(clientHandle, FileAccess.Read, bufferSize: 3, isAsync: true))
295                 {
296                     await serverTask;
297 
298                     var cts = new CancellationTokenSource();
299                     Task clientTask = client.CopyToAsync(new MemoryStream(), 0x1000, cts.Token);
300                     Assert.False(clientTask.IsCompleted);
301 
302                     cts.Cancel();
303                     await Assert.ThrowsAsync<OperationCanceledException>(() => clientTask);
304                 }
305             }
306         }
307 
308         [Theory]
309         [InlineData(false)]
310         [InlineData(true)]
DerivedFileStream_ReadAsyncInvoked(bool useAsync)311         public async Task DerivedFileStream_ReadAsyncInvoked(bool useAsync)
312         {
313             var expectedData = new byte[100];
314             new Random(42).NextBytes(expectedData);
315 
316             string srcPath = GetTestFilePath();
317             File.WriteAllBytes(srcPath, expectedData);
318 
319             bool readAsyncInvoked = false;
320             using (var fs = new FileStreamThatOverridesReadAsync(srcPath, useAsync, () => readAsyncInvoked = true))
321             {
322                 await fs.CopyToAsync(new MemoryStream());
323                 Assert.True(readAsyncInvoked);
324             }
325         }
326 
327         private class FileStreamThatOverridesReadAsync : FileStream
328         {
329             private readonly Action _readAsyncInvoked;
330 
FileStreamThatOverridesReadAsync(string path, bool useAsync, Action readAsyncInvoked)331             internal FileStreamThatOverridesReadAsync(string path, bool useAsync, Action readAsyncInvoked) :
332                 base(path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, useAsync)
333             {
334                 _readAsyncInvoked = readAsyncInvoked;
335             }
336 
ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)337             public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
338             {
339                 _readAsyncInvoked();
340                 return base.ReadAsync(buffer, offset, count, cancellationToken);
341             }
342         }
343 
344         #region Windows P/Invokes
345         // We need to P/Invoke to test the named pipe async behavior with FileStream
346         // because NamedPipeClientStream internally binds the created handle,
347         // and that then prevents FileStream's constructor from working with the handle
348         // when trying to set isAsync to true.
349 
350         [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
351         [return: MarshalAs(UnmanagedType.Bool)]
WaitNamedPipeW(string name, int timeout)352         public static extern bool WaitNamedPipeW(string name, int timeout);
353 
354         [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
CreateFileW( string lpFileName, int dwDesiredAccess, FileShare dwShareMode, IntPtr securityAttrs, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile)355         internal static extern SafeFileHandle CreateFileW(
356             string lpFileName, int dwDesiredAccess, FileShare dwShareMode,
357             IntPtr securityAttrs, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
358 
359         internal const int GENERIC_READ = unchecked((int)0x80000000);
360         #endregion
361     }
362 }
363