Created
June 21, 2012 17:32
-
-
Save markrendle/2967190 to your computer and use it in GitHub Desktop.
Multipart parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace MultipartSpike | |
{ | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Text; | |
public class MultipartParser | |
{ | |
private const int MaximumMemoryStreamSize = 1024*64; | |
private static readonly HashSet<byte> EndOfLine = new HashSet<byte>(new[] { LF, CR }); | |
private readonly Stream _source; | |
private const byte LF = (byte)'\n'; | |
private const byte CR = (byte) '\r'; | |
private readonly byte[] _boundary; | |
private readonly int _boundaryLength; | |
private readonly byte[] _check; | |
private int _byte = -1; | |
private Stream _buffer = Stream.Null; | |
private bool _atEndOfFile; | |
public MultipartParser(Stream source, string boundary) | |
{ | |
_source = source; | |
if (source == null) throw new ArgumentNullException("source"); | |
if (boundary == null) throw new ArgumentNullException("boundary"); | |
if (!source.CanRead) throw new ArgumentException("Stream specificed by source must be readable."); | |
_boundary = Encoding.UTF8.GetBytes("--" + boundary); | |
_boundaryLength = _boundary.Length; | |
_check = new byte[_boundaryLength + 2]; | |
_check[0] = _boundary[0]; | |
} | |
public IEnumerable<PostedFile> Parse() | |
{ | |
if (_source.Length == 0) yield break; | |
if (!MovePastNextBoundary()) yield break; | |
while (!_atEndOfFile) | |
{ | |
var file = GetNextFile(); | |
if (file == null) break; | |
yield return file; | |
} | |
} | |
private PostedFile GetNextFile() | |
{ | |
string fileName; | |
while (!TryGetFileName(out fileName)) | |
{ | |
if (!MovePastNextBoundary()) | |
{ | |
return null; | |
} | |
} | |
var headers = GetHeaders(); | |
_buffer = new MemoryStream(); | |
if (MovePastNextBoundary()) | |
{ | |
_buffer.Position = 0; | |
return new PostedFile(fileName, headers, _buffer); | |
} | |
return null; | |
} | |
private bool MovePastNextBoundary() | |
{ | |
int b, cr = 0, lf = 0; | |
bool atStartOfLine = true; | |
while ((b = _source.ReadByte()) > -1) | |
{ | |
// if b is CR or LF, hold onto it | |
if (b == CR) | |
{ | |
cr = BufferNewlineChar(cr, b, CR); | |
atStartOfLine = true; | |
} | |
else if (b == LF) | |
{ | |
lf = BufferNewlineChar(lf, b, LF); | |
atStartOfLine = true; | |
} | |
else if (atStartOfLine) | |
{ | |
atStartOfLine = false; | |
if (b == _boundary[0]) | |
{ | |
int checkIndex; | |
for (checkIndex = 1; checkIndex < _boundaryLength; checkIndex++) | |
{ | |
b = _source.ReadByte(); | |
if (b < 0) return false; | |
_check[checkIndex] = (byte)b; | |
if (b != _boundary[checkIndex]) | |
{ | |
break; | |
} | |
} | |
if (checkIndex == _boundaryLength) | |
{ | |
_check[checkIndex++] = (byte)_source.ReadByte(); | |
_check[checkIndex++] = (byte)_source.ReadByte(); | |
if (AtBoundary()) | |
{ | |
return true; | |
} | |
WriteBufferBytes(_check, 0, checkIndex); | |
} | |
else | |
{ | |
WriteBufferBytes(_check, 0, checkIndex + 1); | |
} | |
} | |
else | |
{ | |
if (cr > 0) WriteBufferByte(CR); | |
if (lf > 0) WriteBufferByte(LF); | |
WriteBufferByte((byte)b); | |
cr = lf = 0; | |
} | |
} | |
else | |
{ | |
if (cr > 0) WriteBufferByte(CR); | |
if (lf > 0) WriteBufferByte(LF); | |
WriteBufferByte((byte)b); | |
cr = lf = 0; | |
} | |
} | |
_atEndOfFile = true; | |
return false; | |
} | |
// WriteBufferByte and WriteBufferBytes could change MemoryStream to FileStream | |
private void WriteBufferByte(byte b) | |
{ | |
_buffer.WriteByte(b); | |
if (_buffer.Length > MaximumMemoryStreamSize) | |
{ | |
SwitchToTempFileStream(); | |
} | |
} | |
private void WriteBufferBytes(byte[] b, int offset, int count) | |
{ | |
_buffer.Write(b, offset, count); | |
if (_buffer.Length > MaximumMemoryStreamSize) | |
{ | |
SwitchToTempFileStream(); | |
} | |
} | |
private void SwitchToTempFileStream() | |
{ | |
var tempFileStream = TempFileStream.New(); | |
_buffer.CopyTo(tempFileStream); | |
_buffer.Dispose(); | |
_buffer = tempFileStream; | |
} | |
private int BufferNewlineChar(int bx, int b, byte newlineChar) | |
{ | |
if (bx == newlineChar) | |
{ | |
_buffer.WriteByte(newlineChar); | |
} | |
else | |
{ | |
bx = b; | |
} | |
return bx; | |
} | |
private bool AtBoundary() | |
{ | |
for (int i = 0; i < _boundaryLength; i++) | |
{ | |
if (_check[i] != _boundary[i]) | |
{ | |
return false; | |
} | |
} | |
return true; | |
} | |
private IDictionary<string, string> GetHeaders() | |
{ | |
byte[] chunk; | |
var headers = new Dictionary<string, string>(); | |
while (!EndOfLine.Contains((chunk = GetLineBytes())[0])) | |
{ | |
var line = Encoding.UTF8.GetString(chunk); | |
var pair = GetPair(line); | |
if (pair.Key != null) | |
{ | |
headers.Add(pair.Key, pair.Value); | |
} | |
} | |
return headers; | |
} | |
private bool TryGetFileName(out string fileName) | |
{ | |
fileName = null; | |
var chunk = GetLineBytes(); | |
if (chunk == null) return false; | |
var line = Encoding.UTF8.GetString(chunk); | |
if (!line.StartsWith("Content-Disposition")) return false; | |
if (!TryGetFileName(line, out fileName)) return false; | |
return true; | |
} | |
public static bool TryGetFileName(string line, out string fileName) | |
{ | |
fileName = null; | |
const string start = "; filename=\""; | |
const int offset = 12; | |
int filenameIndex = line.IndexOf(start, StringComparison.OrdinalIgnoreCase); | |
if (filenameIndex < 0) return false; | |
line = line.Substring(filenameIndex + offset); | |
int quoteIndex = line.IndexOf('"'); | |
if (quoteIndex < 0) throw new InvalidOperationException("Unterminated filename in multipart content."); | |
fileName = line.Substring(0, quoteIndex); | |
return true; | |
} | |
public static KeyValuePair<string,string> GetPair(string line) | |
{ | |
int colon = line.IndexOf(':'); | |
if (colon < 0) return new KeyValuePair<string, string>(null, null); | |
return new KeyValuePair<string, string>(line.Substring(0, colon), line.Substring(colon + 1).Trim()); | |
} | |
private byte[] GetLineBytes() | |
{ | |
if (_byte == -1) | |
{ | |
_byte = _source.ReadByte(); | |
} | |
if (_byte < 0) return null; | |
using (var buffer = new MemoryStream()) | |
{ | |
while (_byte >= 0 && _byte != LF && _byte != CR) | |
{ | |
buffer.WriteByte((byte)_byte); | |
_byte = _source.ReadByte(); | |
} | |
if (_byte > 0) | |
{ | |
buffer.WriteByte((byte) _byte); | |
if (_byte == CR) | |
{ | |
_byte = _source.ReadByte(); | |
if (_byte == LF) | |
{ | |
buffer.WriteByte(LF); | |
_byte = -1; | |
} | |
} | |
} | |
return buffer.GetBuffer(); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace MultipartSpike | |
{ | |
using System.Collections.Generic; | |
using System.IO; | |
public class PostedFile | |
{ | |
private readonly IDictionary<string, string> _headers; | |
private readonly string _fileName; | |
private readonly Stream _stream; | |
public PostedFile(string fileName, IDictionary<string,string> headers, Stream stream) | |
{ | |
_fileName = fileName; | |
_headers = headers; | |
_stream = stream; | |
} | |
public string FileName | |
{ | |
get { return _fileName; } | |
} | |
public Stream Stream | |
{ | |
get { return _stream; } | |
} | |
public IDictionary<string, string> Headers | |
{ | |
get { return _headers; } | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace MultipartSpike | |
{ | |
using System; | |
using System.IO; | |
internal class TempFileStream : Stream | |
{ | |
private readonly string _file; | |
private Stream _fileStream; | |
public override void Close() | |
{ | |
_fileStream.Close(); | |
} | |
public override void Flush() | |
{ | |
_fileStream.Flush(); | |
} | |
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) | |
{ | |
return _fileStream.BeginRead(buffer, offset, count, callback, state); | |
} | |
public override int EndRead(IAsyncResult asyncResult) | |
{ | |
return _fileStream.EndRead(asyncResult); | |
} | |
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) | |
{ | |
return _fileStream.BeginWrite(buffer, offset, count, callback, state); | |
} | |
public override void EndWrite(IAsyncResult asyncResult) | |
{ | |
_fileStream.EndWrite(asyncResult); | |
} | |
public override long Seek(long offset, SeekOrigin origin) | |
{ | |
return _fileStream.Seek(offset, origin); | |
} | |
public override void SetLength(long value) | |
{ | |
_fileStream.SetLength(value); | |
} | |
public override int Read(byte[] buffer, int offset, int count) | |
{ | |
return _fileStream.Read(buffer, offset, count); | |
} | |
public override int ReadByte() | |
{ | |
return _fileStream.ReadByte(); | |
} | |
public override void Write(byte[] buffer, int offset, int count) | |
{ | |
_fileStream.Write(buffer, offset, count); | |
} | |
public override void WriteByte(byte value) | |
{ | |
_fileStream.WriteByte(value); | |
} | |
public override bool CanRead | |
{ | |
get { return _fileStream.CanRead; } | |
} | |
public override bool CanSeek | |
{ | |
get { return _fileStream.CanSeek; } | |
} | |
public override bool CanTimeout | |
{ | |
get { return _fileStream.CanTimeout; } | |
} | |
public override bool CanWrite | |
{ | |
get { return _fileStream.CanWrite; } | |
} | |
public override long Length | |
{ | |
get { return _fileStream.Length; } | |
} | |
public override long Position | |
{ | |
get { return _fileStream.Position; } | |
set { _fileStream.Position = value; } | |
} | |
public override int ReadTimeout | |
{ | |
get { return _fileStream.ReadTimeout; } | |
set { _fileStream.ReadTimeout = value; } | |
} | |
public override int WriteTimeout | |
{ | |
get { return _fileStream.WriteTimeout; } | |
set { _fileStream.WriteTimeout = value; } | |
} | |
private TempFileStream(string file, Stream fileStream) | |
{ | |
_file = file; | |
_fileStream = fileStream; | |
} | |
public static TempFileStream New() | |
{ | |
var file = Path.GetTempFileName(); | |
var fileStream = System.IO.File.Open(file, FileMode.Open, FileAccess.ReadWrite); | |
return new TempFileStream(file, fileStream); | |
} | |
protected override void Dispose(bool disposing) | |
{ | |
if (_fileStream != null) | |
{ | |
_fileStream.Dispose(); | |
_fileStream = null; | |
} | |
if (System.IO.File.Exists(_file)) | |
{ | |
try | |
{ | |
System.IO.File.Delete(_file); | |
} | |
catch | |
{ | |
} | |
} | |
base.Dispose(disposing); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment