unit fpsStreams; {$ifdef fpc} {$mode delphi}{$H+} {$endif} interface uses SysUtils, Classes; var DEFAULT_STREAM_BUFFER_SIZE: Integer = 1024 * 1024; // 1 MB type { A buffered stream } TBufStream = class(TStream) private FFileStream: TFileStream; FMemoryStream: TMemoryStream; FFileStreamPos: Int64; FFileStreamSize: Int64; FBufWritten: Boolean; FBufSize: Int64; FKeepTmpFile: Boolean; FFileName: String; FFileMode: Word; protected procedure CreateFileStream; function GetPosition: Int64; override; function GetSize: Int64; override; function IsWritingMode: Boolean; public constructor Create(AFileName: String; AMode: Word; ABufSize: Cardinal = Cardinal(-1)); overload; constructor Create(ATempFile: String; AKeepFile: Boolean = false; ABufSize: Cardinal = Cardinal(-1)); overload; constructor Create(ABufSize: Cardinal = Cardinal(-1)); overload; destructor Destroy; override; procedure FillBuffer; procedure FlushBuffer; function Read(var Buffer; Count: Longint): Longint; override; function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override; function Write(const ABuffer; ACount: Longint): Longint; override; end; procedure ResetStream(var AStream: TStream); implementation uses Math; { Resets the stream position to the beginning of the stream. } procedure ResetStream(var AStream: TStream); begin if AStream <> nil then AStream.Position := 0; end; {@@ Constructor of the TBufStream. Creates a memory stream and prepares everything to create also a file stream if the stream size exceeds ABufSize bytes. @param ATempFile File name for the file stream. If an empty string is used a temporary file name is created by calling GetTempFileName. @param AKeepFile If true and the stream is in WritingMode the stream is flushed to file when the stream is destroyed. If false the file is deleted when the stream is destroyed. @param ABufSize Maximum size of the memory stream before swapping to file starts. Value is given in bytes. } constructor TBufStream.Create(ATempFile: String; AKeepFile: Boolean = false; ABufSize: Cardinal = Cardinal(-1)); begin if ATempFile = '' then ATempFile := ChangeFileExt(GetTempFileName, '.~abc'); // Change extension because of naming conflict if the name of the main file // is determined by GetTempFileName also. Happens in internaltests suite. FFileName := ATempFile; FKeepTmpFile := AKeepFile; FMemoryStream := TMemoryStream.Create; // The file stream is only created when needed because of possible conflicts // of random file names. if ABufSize = Cardinal(-1) then FBufSize := DEFAULT_STREAM_BUFFER_SIZE else FBufSize := ABufSize; FFileMode := fmCreate + fmOpenRead; end; {@@ Constructor of the TBufStream. Creates a memory stream and prepares everything to create also a file stream if the streamsize exceeds ABufSize bytes. The stream created by this constructor is mainly intended to serve a temporary purpose, it is not stored permanently to file. @param ABufSize Maximum size of the memory stream before swapping to file starts. Value is given in bytes. } constructor TBufStream.Create(ABufSize: Cardinal = Cardinal(-1)); begin Create('', false, ABufSize); end; {@@ Constructor of the TBufStream. When swapping to file it will create a file stream using the given file mode. This kind of BufStream is considered as a fast replacement of TFileStream. @param AFileName File name for the file stream. If an empty string is used a temporary file name is created by calling GetTempFileName. @param AMode FileMode for the file stream (fmCreate, fmOpenRead etc.) @param ABufSize Maximum size of the memory stream before swapping to file starts. Value is given in bytes. } constructor TBufStream.Create(AFileName: String; AMode: Word; ABufSize: Cardinal = Cardinal(-1)); var keep: Boolean; begin keep := AMode and (fmCreate + fmOpenWrite) <> 0; Create(AFileName, keep, ABufSize); FFileMode := AMode; end; destructor TBufStream.Destroy; begin // Write current buffer content to file if FKeepTmpFile then FlushBuffer; // Free streams and delete temporary file, if requested FreeAndNil(FMemoryStream); FreeAndNil(FFileStream); if not FKeepTmpFile and (FFileName <> '') and IsWritingMode then DeleteFile(FFileName); inherited Destroy; end; { Creation of the file stream is delayed because of naming conflicts of other streams are needed with random file names as well (the files do not yet exist when the streams are created and therefore get the same name by GetTempFileName! } procedure TBufStream.CreateFileStream; begin if FFileStream = nil then begin if FFileName = '' then FFileName := ChangeFileExt(GetTempFileName, '.~abc'); FFileStream := TFileStream.Create(FFileName, FFileMode); FFileStreamSize := FFileStream.Size; FFileStreamPos := 0; end; end; { Reads FBufSize bytes from the stream into the buffer. Called when reading. } procedure TBufStream.FillBuffer; var p, n: Int64; begin p := GetPosition; FMemoryStream.Clear; FMemoryStream.Position := 0; FFileStream.Position := p; n := Min(FBufSize, FFileStreamSize - p); FMemoryStream.CopyFrom(FFileStream, n); FMemoryStream.Position := 0; FFileStream.Position := p; // The file stream ends where the memorystream begins! FFileStreamPos := p; end; { Flushes the contents of the memory stream to file Called when writing. } procedure TBufStream.FlushBuffer; begin if (FMemoryStream.Size > 0) and not FBufWritten and IsWritingMode then begin FMemoryStream.Position := 0; CreateFileStream; FFileStream.CopyFrom(FMemoryStream, FMemoryStream.Size); FFileStreamPos := FFileStream.Position; FFileStreamSize := FFileStream.Size; FMemoryStream.Clear; FBufWritten := true; end; end; { Returns the buffer position. This is the buffer position of the bytes written to file, plus the current position in the memory buffer } function TBufStream.GetPosition: Int64; begin if FFileStream = nil then Result := FMemoryStream.Position else // Result := FFileStream.Position + FMemoryStream.Position; Result := FFileStreamPos + FMemoryStream.Position; end; { Returns the size of the stream. Both memory and file streams are considered if needed. } function TBufStream.GetSize: Int64; var n: Int64; begin if IsWritingMode then begin if FFileStream <> nil then n := FFileStreamSize // n := FFileStream.Size else n := 0; if n = 0 then n := FMemoryStream.Size; Result := Max(n, GetPosition); end else begin CreateFileStream; Result := FFileStreamSize; end; end; {@@ Returns true if the stream is in WritingMode. "WritingMode" means that the stream is primarily used for writing. The memory stream is initially empty but fills during writing, it is written to disk when it is full. The (unnamend) opposite of "WritingMode" indicates that the stream is used for reading. The memory stream is initially full, but the stream pointer is at it start. When data are read the stream pointer advances towards the end. When the requested data are not contained in the memory stream another ABufSize of bytes are read into the memory stream. } function TBufStream.IsWritingMode: Boolean; begin Result := FFileMode and (fmCreate + fmOpenWrite) <> 0; end; {@@ Reads a given number of bytes into a buffer and return the number of bytes read. If the bytes are not in the memory stream they are read from the file stream. @param Buffer Buffer into which the bytes are read. Sufficient space must have been allocated for Count bytes @param Count Number of bytes to read from the stream @return Number of bytes that were read from the stream.} function TBufStream.Read(var Buffer; Count: Longint): Longint; var p: Int64; begin p := GetPosition; // Save stream position // Case 1: Memory stream is empty if FMemoryStream.Size = 0 then begin CreateFileStream; if IsWritingMode then begin Result := FFileStream.Read(Buffer, Count); FFileStreamPos := FFileStream.Position; end else begin FillBuffer; Result := FMemoryStream.Read(Buffer, Count); end; exit; end; // Case 2: All "Count" bytes are contained in memory stream starting at current position if FMemoryStream.Position + Count <= FMemoryStream.Size then begin Result := FMemoryStream.Read(Buffer, Count); exit; end; // Case 3: Memory stream is not empty but contains only part of the bytes requested if IsWritingMode then begin FlushBuffer; FFileStream.Position := p; Result := FFileStream.Read(Buffer, Count); FFileStreamPos := p + Count; end else begin FillBuffer; Result := FMemoryStream.Read(Buffer, Count); end; end; function TBufStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; var oldPos: Int64; newPos: Int64; begin oldPos := GetPosition; case Origin of soBeginning : newPos := Offset; soCurrent : newPos := oldPos + Offset; soEnd : newPos := GetSize - Offset; end; // case #1: New position is within buffer, no file stream yet if (FFileStream = nil) and (newPos < FMemoryStream.Size) then begin FMemoryStream.Position := newPos; Result := FMemoryStream.Position; exit; end; CreateFileStream; // case #2: New position is within buffer, file stream exists // if (newPos >= FFileStream.Position) and (newPos < FFileStream.Position + FMemoryStream.Size) if (newPos >= FFileStreamPos) and (newPos < FFileStreamPos + FMemoryStream.Size) then begin // FMemoryStream.Position := newPos - FFileStream.Position; FMemoryStream.Position := newPos - FFileStreamPos; Result := newpos; //FMemoryStream.Position; exit; end; // case #3: New position is outside buffer if IsWritingMode then FlushBuffer; FFileStream.Position := newPos; FFileStreamPos := newPos; FMemoryStream.Position := 0; if not IsWritingMode then FillBuffer; end; function TBufStream.Write(const ABuffer; ACount: LongInt): LongInt; var savedPos: Int64; begin // Case #1: Bytes fit into buffer if FMemoryStream.Position + ACount < FBufSize then begin Result := FMemoryStream.Write(ABuffer, ACount); FBufWritten := false; end else // Case #2: Buffer would overflow begin; savedPos := GetPosition; FlushBuffer; FFileStream.Position := savedPos; Result := FFileStream.Write(ABuffer, ACount); FFileStreamPos := savedPos + ACount; FFileStreamSize := FFileStream.Size; end; end; end.