Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit 15a51e4

Browse files
committed
#175 - Decode multipart headers as UTF-8.
1 parent 97c9f8f commit 15a51e4

File tree

2 files changed

+131
-21
lines changed

2 files changed

+131
-21
lines changed

src/Microsoft.AspNet.WebUtilities/BufferedReadStream.cs

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ namespace Microsoft.AspNet.WebUtilities
1111
{
1212
internal class BufferedReadStream : Stream
1313
{
14-
private const char CR = '\r';
15-
private const char LF = '\n';
14+
private const byte CR = (byte)'\r';
15+
private const byte LF = (byte)'\n';
1616

1717
private readonly Stream _inner;
1818
private readonly byte[] _buffer;
@@ -310,8 +310,9 @@ public async Task<bool> EnsureBufferedAsync(int minCount, CancellationToken canc
310310
public string ReadLine(int lengthLimit)
311311
{
312312
CheckDisposed();
313-
StringBuilder builder = new StringBuilder();
313+
var builder = new MemoryStream(200);
314314
bool foundCR = false, foundCRLF = false;
315+
315316
while (!foundCRLF && EnsureBuffered())
316317
{
317318
if (builder.Length > lengthLimit)
@@ -321,19 +322,15 @@ public string ReadLine(int lengthLimit)
321322
ProcessLineChar(builder, ref foundCR, ref foundCRLF);
322323
}
323324

324-
if (foundCRLF)
325-
{
326-
return builder.ToString(0, builder.Length - 2); // Drop the CRLF
327-
}
328-
// Stream ended with no CRLF.
329-
return builder.ToString();
325+
return DecodeLine(builder, foundCRLF);
330326
}
331327

332328
public async Task<string> ReadLineAsync(int lengthLimit, CancellationToken cancellationToken)
333329
{
334330
CheckDisposed();
335-
StringBuilder builder = new StringBuilder();
331+
var builder = new MemoryStream(200);
336332
bool foundCR = false, foundCRLF = false;
333+
337334
while (!foundCRLF && await EnsureBufferedAsync(cancellationToken))
338335
{
339336
if (builder.Length > lengthLimit)
@@ -344,25 +341,20 @@ public async Task<string> ReadLineAsync(int lengthLimit, CancellationToken cance
344341
ProcessLineChar(builder, ref foundCR, ref foundCRLF);
345342
}
346343

347-
if (foundCRLF)
348-
{
349-
return builder.ToString(0, builder.Length - 2); // Drop the CRLF
350-
}
351-
// Stream ended with no CRLF.
352-
return builder.ToString();
344+
return DecodeLine(builder, foundCRLF);
353345
}
354346

355-
private void ProcessLineChar(StringBuilder builder, ref bool foundCR, ref bool foundCRLF)
347+
private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF)
356348
{
357-
char ch = (char)_buffer[_bufferOffset]; // TODO: Encoding enforcement
358-
builder.Append(ch);
349+
var b = _buffer[_bufferOffset];
350+
builder.WriteByte(b);
359351
_bufferOffset++;
360352
_bufferCount--;
361-
if (ch == CR)
353+
if (b == CR)
362354
{
363355
foundCR = true;
364356
}
365-
else if (ch == LF)
357+
else if (b == LF)
366358
{
367359
if (foundCR)
368360
{
@@ -375,6 +367,13 @@ private void ProcessLineChar(StringBuilder builder, ref bool foundCR, ref bool f
375367
}
376368
}
377369

370+
private string DecodeLine(MemoryStream builder, bool foundCRLF)
371+
{
372+
// Drop the final CRLF, if any
373+
var length = foundCRLF ? builder.Length - 2 : builder.Length;
374+
return Encoding.UTF8.GetString(builder.ToArray(), 0, (int)length);
375+
}
376+
378377
private void CheckDisposed()
379378
{
380379
if (_disposed)

test/Microsoft.AspNet.WebUtilities.Tests/MultipartReaderTests.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ public class MultipartReaderTests
4343
4444
Content of a.txt.
4545
46+
--9051914041544843365972754266--
47+
";
48+
private const string TwoPartBodyWithUnicodeFileName =
49+
@"--9051914041544843365972754266
50+
Content-Disposition: form-data; name=""text""
51+
52+
text default
53+
--9051914041544843365972754266
54+
Content-Disposition: form-data; name=""file1""; filename=""a色.txt""
55+
Content-Type: text/plain
56+
57+
Content of a.txt.
58+
4659
--9051914041544843365972754266--
4760
";
4861
private const string ThreePartBody =
@@ -147,6 +160,32 @@ public async Task MutipartReader_ReadTwoPartBody_Success()
147160
Assert.Null(await reader.ReadNextSectionAsync());
148161
}
149162

163+
[Fact]
164+
public async Task MutipartReader_ReadTwoPartBodyWithUnicodeFileName_Success()
165+
{
166+
var stream = MakeStream(TwoPartBodyWithUnicodeFileName);
167+
var reader = new MultipartReader(Boundary, stream);
168+
169+
var section = await reader.ReadNextSectionAsync();
170+
Assert.NotNull(section);
171+
Assert.Equal(1, section.Headers.Count);
172+
Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]);
173+
var buffer = new MemoryStream();
174+
await section.Body.CopyToAsync(buffer);
175+
Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray()));
176+
177+
section = await reader.ReadNextSectionAsync();
178+
Assert.NotNull(section);
179+
Assert.Equal(2, section.Headers.Count);
180+
Assert.Equal("form-data; name=\"file1\"; filename=\"a色.txt\"", section.Headers["Content-Disposition"][0]);
181+
Assert.Equal("text/plain", section.Headers["Content-Type"][0]);
182+
buffer = new MemoryStream();
183+
await section.Body.CopyToAsync(buffer);
184+
Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray()));
185+
186+
Assert.Null(await reader.ReadNextSectionAsync());
187+
}
188+
150189
[Fact]
151190
public async Task MutipartReader_ThreePartBody_Success()
152191
{
@@ -181,5 +220,77 @@ public async Task MutipartReader_ThreePartBody_Success()
181220

182221
Assert.Null(await reader.ReadNextSectionAsync());
183222
}
223+
224+
[Fact]
225+
public async Task MutipartReader_ReadInvalidUtf8Header_ReplacementCharacters()
226+
{
227+
var body1 =
228+
@"--9051914041544843365972754266
229+
Content-Disposition: form-data; name=""text"" filename=""a";
230+
231+
var body2 =
232+
@".txt""
233+
234+
text default
235+
--9051914041544843365972754266--
236+
";
237+
var stream = new MemoryStream();
238+
var bytes = Encoding.UTF8.GetBytes(body1);
239+
stream.Write(bytes, 0, bytes.Length);
240+
241+
// Write an invalid utf-8 segment in the middle
242+
stream.Write(new byte[] { 0xC1, 0x21 }, 0, 2);
243+
244+
bytes = Encoding.UTF8.GetBytes(body2);
245+
stream.Write(bytes, 0, bytes.Length);
246+
stream.Seek(0, SeekOrigin.Begin);
247+
var reader = new MultipartReader(Boundary, stream);
248+
249+
var section = await reader.ReadNextSectionAsync();
250+
Assert.NotNull(section);
251+
Assert.Equal(1, section.Headers.Count);
252+
Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD!.txt\"", section.Headers["Content-Disposition"][0]);
253+
var buffer = new MemoryStream();
254+
await section.Body.CopyToAsync(buffer);
255+
Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray()));
256+
257+
Assert.Null(await reader.ReadNextSectionAsync());
258+
}
259+
260+
[Fact]
261+
public async Task MutipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters()
262+
{
263+
var body1 =
264+
@"--9051914041544843365972754266
265+
Content-Disposition: form-data; name=""text"" filename=""a";
266+
267+
var body2 =
268+
@".txt""
269+
270+
text default
271+
--9051914041544843365972754266--
272+
";
273+
var stream = new MemoryStream();
274+
var bytes = Encoding.UTF8.GetBytes(body1);
275+
stream.Write(bytes, 0, bytes.Length);
276+
277+
// Write an invalid utf-8 segment in the middle
278+
stream.Write(new byte[] { 0xED, 0xA0, 85 }, 0, 3);
279+
280+
bytes = Encoding.UTF8.GetBytes(body2);
281+
stream.Write(bytes, 0, bytes.Length);
282+
stream.Seek(0, SeekOrigin.Begin);
283+
var reader = new MultipartReader(Boundary, stream);
284+
285+
var section = await reader.ReadNextSectionAsync();
286+
Assert.NotNull(section);
287+
Assert.Equal(1, section.Headers.Count);
288+
Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFDU.txt\"", section.Headers["Content-Disposition"][0]);
289+
var buffer = new MemoryStream();
290+
await section.Body.CopyToAsync(buffer);
291+
Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray()));
292+
293+
Assert.Null(await reader.ReadNextSectionAsync());
294+
}
184295
}
185296
}

0 commit comments

Comments
 (0)