// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // // System.Net.ChunkStream // // Authors: // Gonzalo Paniagua Javier (gonzalo@ximian.com) // // (C) 2003 Ximian, Inc (http://www.ximian.com) // // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // ReSharper disable RedundantUsingDirective // ReSharper disable ConditionIsAlwaysTrueOrFalse using System; using System.Collections; using System.Diagnostics; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Text; namespace SpaceWizards.HttpListener { internal sealed class ChunkStream { private enum State { None, PartialSize, Body, BodyFinished, Trailer } private sealed class Chunk { public byte[] Bytes; public int Offset; public Chunk(byte[] chunk) { Bytes = chunk; } public int Read(byte[] buffer, int offset, int size) { int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size; Buffer.BlockCopy(Bytes, Offset, buffer, offset, nread); Offset += nread; return nread; } } internal WebHeaderCollection _headers; private int _chunkSize; private int _chunkRead; private int _totalWritten; private State _state; private StringBuilder _saved; private bool _sawCR; private bool _gotit; private int _trailerState; private List _chunks; public ChunkStream(WebHeaderCollection headers) { _headers = headers; _saved = new StringBuilder(); _chunks = new List(); _chunkSize = -1; _totalWritten = 0; } public void ResetBuffer() { _chunkSize = -1; _chunkRead = 0; _totalWritten = 0; _chunks.Clear(); } public int Read(byte[] buffer, int offset, int size) { return ReadFromChunks(buffer, offset, size); } private int ReadFromChunks(byte[] buffer, int offset, int size) { int count = _chunks.Count; int nread = 0; var chunksForRemoving = new List(count); for (int i = 0; i < count; i++) { Chunk chunk = _chunks[i]; if (chunk.Offset == chunk.Bytes.Length) { chunksForRemoving.Add(chunk); continue; } nread += chunk.Read(buffer, offset + nread, size - nread); if (nread == size) break; } foreach (var chunk in chunksForRemoving) _chunks.Remove(chunk); return nread; } public void Write(byte[] buffer, int offset, int size) { // Note, the logic here only works when offset is 0 here. // Otherwise, it would treat "size" as the end offset instead of an actual byte count from offset. Debug.Assert(offset == 0); if (offset < size) InternalWrite(buffer, ref offset, size); } private void InternalWrite(byte[] buffer, ref int offset, int size) { if (_state == State.None || _state == State.PartialSize) { _state = GetChunkSize(buffer, ref offset, size); if (_state == State.PartialSize) return; _saved.Length = 0; _sawCR = false; _gotit = false; } if (_state == State.Body && offset < size) { _state = ReadBody(buffer, ref offset, size); if (_state == State.Body) return; } if (_state == State.BodyFinished && offset < size) { _state = ReadCRLF(buffer, ref offset, size); if (_state == State.BodyFinished) return; _sawCR = false; } if (_state == State.Trailer && offset < size) { _state = ReadTrailer(buffer, ref offset, size); if (_state == State.Trailer) return; _saved.Length = 0; _sawCR = false; _gotit = false; } if (offset < size) InternalWrite(buffer, ref offset, size); } public bool WantMore { get { return (_chunkRead != _chunkSize || _chunkSize != 0 || _state != State.None); } } public bool DataAvailable { get { int count = _chunks.Count; for (int i = 0; i < count; i++) { Chunk ch = _chunks[i]; if (ch == null || ch.Bytes == null) continue; if (ch.Bytes.Length > 0 && ch.Offset < ch.Bytes.Length) return (_state != State.Body); } return false; } } public int TotalDataSize { get { return _totalWritten; } } public int ChunkLeft { get { return _chunkSize - _chunkRead; } } private State ReadBody(byte[] buffer, ref int offset, int size) { if (_chunkSize == 0) return State.BodyFinished; int diff = size - offset; if (diff + _chunkRead > _chunkSize) diff = _chunkSize - _chunkRead; byte[] chunk = new byte[diff]; Buffer.BlockCopy(buffer, offset, chunk, 0, diff); _chunks.Add(new Chunk(chunk)); offset += diff; _chunkRead += diff; _totalWritten += diff; return (_chunkRead == _chunkSize) ? State.BodyFinished : State.Body; } private State GetChunkSize(byte[] buffer, ref int offset, int size) { _chunkRead = 0; _chunkSize = 0; char c = '\0'; while (offset < size) { c = (char)buffer[offset++]; if (c == '\r') { if (_sawCR) ThrowProtocolViolation("2 CR found"); _sawCR = true; continue; } if (_sawCR && c == '\n') break; if (c == ' ') _gotit = true; if (!_gotit) _saved.Append(c); if (_saved.Length > 20) ThrowProtocolViolation("chunk size too long."); } if (!_sawCR || c != '\n') { if (offset < size) ThrowProtocolViolation("Missing \\n"); try { if (_saved.Length > 0) { _chunkSize = int.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber, CultureInfo.InvariantCulture); } } catch (Exception) { ThrowProtocolViolation("Cannot parse chunk size."); } return State.PartialSize; } _chunkRead = 0; try { _chunkSize = int.Parse(RemoveChunkExtension(_saved.ToString()), NumberStyles.HexNumber, CultureInfo.InvariantCulture); } catch (Exception) { ThrowProtocolViolation("Cannot parse chunk size."); } if (_chunkSize == 0) { _trailerState = 2; return State.Trailer; } return State.Body; } private static string RemoveChunkExtension(string input) { int idx = input.IndexOf(';'); if (idx == -1) return input; return input.Substring(0, idx); } private State ReadCRLF(byte[] buffer, ref int offset, int size) { if (!_sawCR) { if ((char)buffer[offset++] != '\r') ThrowProtocolViolation("Expecting \\r"); _sawCR = true; if (offset == size) return State.BodyFinished; } if (_sawCR && (char)buffer[offset++] != '\n') ThrowProtocolViolation("Expecting \\n"); return State.None; } private State ReadTrailer(byte[] buffer, ref int offset, int size) { char c = '\0'; // short path if (_trailerState == 2 && (char)buffer[offset] == '\r' && _saved.Length == 0) { offset++; if (offset < size && (char)buffer[offset] == '\n') { offset++; return State.None; } offset--; } int st = _trailerState; string stString = "\r\n\r"; while (offset < size && st < 4) { c = (char)buffer[offset++]; if ((st == 0 || st == 2) && c == '\r') { st++; continue; } if ((st == 1 || st == 3) && c == '\n') { st++; continue; } if (st > 0) { #if UNITY_NETFRAMEWORK _saved.Append(stString.Substring(0, _saved.Length == 0 ? st - 2 : st)); #else _saved.Append(stString.AsSpan(0, _saved.Length == 0 ? st - 2 : st)); #endif st = 0; if (_saved.Length > 4196) ThrowProtocolViolation("Error reading trailer (too long)."); } } if (st < 4) { _trailerState = st; if (offset < size) ThrowProtocolViolation("Error reading trailer."); return State.Trailer; } StringReader reader = new StringReader(_saved.ToString()); string? line; while ((line = reader.ReadLine()) != null && line != "") _headers.Add(line); return State.None; } private static void ThrowProtocolViolation(string message) { WebException we = new WebException(message, null, WebExceptionStatus.ServerProtocolViolation, null); throw we; } } }