Chris@0: eof()) { Chris@0: $char = $stream->read(1); Chris@0: Chris@0: if ($crFound && $char === self::LF) { Chris@0: $crFound = false; Chris@0: break; Chris@0: } Chris@0: Chris@0: // CR NOT followed by LF Chris@0: if ($crFound && $char !== self::LF) { Chris@0: throw new UnexpectedValueException('Unexpected carriage return detected'); Chris@0: } Chris@0: Chris@0: // LF in isolation Chris@0: if (! $crFound && $char === self::LF) { Chris@0: throw new UnexpectedValueException('Unexpected line feed detected'); Chris@0: } Chris@0: Chris@0: // CR found; do not append Chris@0: if ($char === self::CR) { Chris@0: $crFound = true; Chris@0: continue; Chris@0: } Chris@0: Chris@0: // Any other character: append Chris@0: $line .= $char; Chris@0: } Chris@0: Chris@0: // CR found at end of stream Chris@0: if ($crFound) { Chris@0: throw new UnexpectedValueException("Unexpected end of headers"); Chris@0: } Chris@0: Chris@0: return $line; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Split the stream into headers and body content. Chris@0: * Chris@0: * Returns an array containing two elements Chris@0: * Chris@0: * - The first is an array of headers Chris@0: * - The second is a StreamInterface containing the body content Chris@0: * Chris@0: * @param StreamInterface $stream Chris@0: * @return array Chris@0: * @throws UnexpectedValueException For invalid headers. Chris@0: */ Chris@0: protected static function splitStream(StreamInterface $stream) Chris@0: { Chris@0: $headers = []; Chris@0: $currentHeader = false; Chris@0: Chris@0: while ($line = self::getLine($stream)) { Chris@0: if (preg_match(';^(?P[!#$%&\'*+.^_`\|~0-9a-zA-Z-]+):(?P.*)$;', $line, $matches)) { Chris@0: $currentHeader = $matches['name']; Chris@0: if (! isset($headers[$currentHeader])) { Chris@0: $headers[$currentHeader] = []; Chris@0: } Chris@0: $headers[$currentHeader][] = ltrim($matches['value']); Chris@0: continue; Chris@0: } Chris@0: Chris@0: if (! $currentHeader) { Chris@0: throw new UnexpectedValueException('Invalid header detected'); Chris@0: } Chris@0: Chris@0: if (! preg_match('#^[ \t]#', $line)) { Chris@0: throw new UnexpectedValueException('Invalid header continuation'); Chris@0: } Chris@0: Chris@0: // Append continuation to last header value found Chris@0: $value = array_pop($headers[$currentHeader]); Chris@0: $headers[$currentHeader][] = $value . ltrim($line); Chris@0: } Chris@0: Chris@0: // use RelativeStream to avoid copying initial stream into memory Chris@0: return [$headers, new RelativeStream($stream, $stream->tell())]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Serialize headers to string values. Chris@0: * Chris@0: * @param array $headers Chris@0: * @return string Chris@0: */ Chris@0: protected static function serializeHeaders(array $headers) Chris@0: { Chris@0: $lines = []; Chris@0: foreach ($headers as $header => $values) { Chris@0: $normalized = self::filterHeader($header); Chris@0: foreach ($values as $value) { Chris@0: $lines[] = sprintf('%s: %s', $normalized, $value); Chris@0: } Chris@0: } Chris@0: Chris@0: return implode("\r\n", $lines); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Filter a header name to wordcase Chris@0: * Chris@0: * @param string $header Chris@0: * @return string Chris@0: */ Chris@0: protected static function filterHeader($header) Chris@0: { Chris@0: $filtered = str_replace('-', ' ', $header); Chris@0: $filtered = ucwords($filtered); Chris@0: return str_replace(' ', '-', $filtered); Chris@0: } Chris@0: }