unit Sparkle.Multipart.FormDataReader;

{$I Sparkle.inc}

interface

uses
  System.SysUtils, System.Classes,
  System.Generics.Collections,
  Sparkle.Utils,
  Sparkle.Multipart.Stream;

type
  TMultipartFormDataPart = class
  private
    FStream: TStream;
    FContentTypeHeader: string;
    FContentDispositionHeader: string;
    FContentDispositionInfo: TContentDispositionInfo;
    FContentTypeInfo: TContentTypeInfo;
    function ReadLine: string;
    function GetParamCount: Integer;
    function GetParamName(Index: Integer): string;
    function GetParamValue(Index: Integer): string;
    function GetFileName: string;
    function GetName: string;
    function GetCharset: string;
    function GetMediaType: string;
    function GetContentDisposition: string;
  protected
    property ContentDispositionHeader: string read FContentDispositionHeader;
    property ContentTypeHeader: string read FContentTypeHeader;
  public
    constructor Create(AStream: TStream);
    procedure LoadHeaders;
    function ParamByName(const Name: string): string;
    property ParamCount: Integer read GetParamCount;
    property ParamName[Index: Integer]: string read GetParamName;
    property ParamValue[Index: Integer]: string read GetParamValue;
    property FileName: string read GetFileName;
    property Name: string read GetName;
    property MediaType: string read GetMediaType;
    property Charset: string read GetCharset;
    property ContentDisposition: string read GetContentDisposition;
  end;

  TMultipartFormDataReader = class
  private
    FStream: TMultipartStream;
    FReader: TStreamReader;
    FCurrent: TMultipartFormDataPart;
  public
    constructor Create(AContent: TStream; const AContentType: string);
    destructor Destroy; override;
    function Next: Boolean;
    procedure ContentToStream(Stream: TStream);
    function ContentAsBytes: TBytes;
    function ContentAsString: string;
    property PartInfo: TMultipartFormDataPart read FCurrent;
  end;

implementation

{ TMultipartFormDataFileInfo }

constructor TMultipartFormDataPart.Create(AStream: TStream);
begin
  FStream := AStream;
end;

function TMultipartFormDataPart.GetCharset: string;
begin
  Result := FContentTypeInfo.ParamValue('charset');
end;

function TMultipartFormDataPart.GetContentDisposition: string;
begin
  Result := FContentDispositionInfo.Value;
end;

function TMultipartFormDataPart.GetFileName: string;
begin
  Result := AnsiDequotedStr(ParamByName('filename'), '"');
end;

function TMultipartFormDataPart.GetMediaType: string;
begin
  Result := FContentTypeInfo.Value;
end;

function TMultipartFormDataPart.GetName: string;
begin
  Result := ParamByName('name');
end;

function TMultipartFormDataPart.GetParamCount: Integer;
begin
  Result := Length(FContentDispositionInfo.Params);
end;

function TMultipartFormDataPart.GetParamName(Index: Integer): string;
begin
  Result := FContentDispositionInfo.Params[Index].Name;
end;

function TMultipartFormDataPart.GetParamValue(Index: Integer): string;
begin
  Result := FContentDispositionInfo.Params[Index].Value;
end;

procedure TMultipartFormDataPart.LoadHeaders;
var
  Line: string;
  HeaderName, HeaderValue: string;
begin
  repeat
    Line := ReadLine;
    TSparkleUtils.ParseHeader(Line, HeaderName, HeaderValue);
    if SameText(HeaderName, 'content-type') then
    begin
      FContentTypeHeader := Trim(HeaderValue);
      FContentTypeInfo := TSparkleUtils.ParseContentType(FContentTypeHeader);
    end
    else
    if SameText(HeaderName, 'content-disposition') then
    begin
      FContentDispositionHeader := Trim(HeaderValue);
      FContentDispositionInfo := TSparkleUtils.ParseContentDisposition(FContentDispositionHeader);
    end;
  until Line = '';
end;

function TMultipartFormDataPart.ParamByName(const Name: string): string;
begin
  Result := FContentDispositionInfo.ParamValue(Name);
end;

function TMultipartFormDataPart.ReadLine: string;
const
  Len = 4192; // maximum length for a line
var
  Bytes: TBytes;
  I: Integer;
  Previous: Byte;
begin
  // slow routine but headers are supposed to be small anyway
  SetLength(Bytes, Len);
  I := 0;
  Previous := 0;
  while I < Len do
  begin
    if FStream.Read(Bytes[I], 1) = 0 then break;
    if (Previous = 13) and (Bytes[I] = 10) then
    begin
      Dec(I);
      break;
    end;
    Previous := Bytes[I];
    Inc(I);
  end;
  Result := TEncoding.UTF8.GetString(Bytes, 0, I);
end;

{ TMultipartFormDataParser }

constructor TMultipartFormDataReader.Create(AContent: TStream; const AContentType: string);
begin
  FStream := TMultipartStream.Create(AContent, AContentType);
  FCurrent := TMultipartFormDataPart.Create(FStream);
  FReader := TStreamReader.Create(FStream);
end;

destructor TMultipartFormDataReader.Destroy;
begin
  FCurrent.Free;
  FStream.Free;
  FReader.Free;
  inherited;
end;

function TMultipartFormDataReader.Next: Boolean;
begin
  FCurrent.Free;
  FCurrent := TMultipartFormDataPart.Create(FStream);
  Result := FStream.NextPart;
  if Result then
    FCurrent.LoadHeaders;
end;

function TMultipartFormDataReader.ContentAsBytes: TBytes;
var
  B: TBytesStream;
begin
  B := TBytesStream.Create;
  try
    ContentToStream(B);
    Result := Copy(B.Bytes, 0, B.Size);
  finally
    B.Free;
  end;
end;

function TMultipartFormDataReader.ContentAsString: string;
begin
  // quick and dirty usage of UTF8 for now. Todo: check for chartset in content type,
  // and/or for a part named _charset_
  Result := TEncoding.UTF8.GetString(ContentAsBytes);
end;

procedure TMultipartFormDataReader.ContentToStream(Stream: TStream);
const
  TempBufferSize = 16384;
var
  Buffer: TBytes;
  BufLen: Int64;
  BytesRead: Cardinal;
begin
  BufLen := TempBufferSize;
  SetLength(Buffer, BufLen);
  repeat
    BytesRead := FStream.Read(Buffer[0], BufLen);
    if BytesRead > 0 then
      Stream.Write(Buffer[0], BytesRead);
  until (BytesRead <> BufLen);
end;

end.
