//------------------------------------------------------------------------------ // ODFileUnit.Pas Copyright (C) 1997 Object Dynamics Ltd. // // This unit implements classes supporting file I/O using Win32 I/O functions, // and a "C-like" I/O style. It is intended to be somewhat easier to use than // the built-in Pascal file I/O mechanisms. // // // *** IMPORTANT *** // // By using this code, you accept the following conditions: // // You may use and adapt this code freely, but it remains the // copyright of Object Dynamics Ltd. Any adaptations must retain the // copyright message at the head of this file. // // You use this code at your own risk. Object Dynamics is not responsible // for any loss or damage caused by programs using this code. // // // History: // // Version 1.0 Created by Neil Butterworth, September 1997 // Fixed problems with file create modes, November 1997. // //------------------------------------------------------------------------------ unit ODFileUnit; interface uses Windows, Messages, SysUtils, Classes; type // Windows file handle FileHandle = integer; // All classes raise this exception FileError = class( Exception ); // Raw file modes FileOpenMode = ( foRead, // open file read-only foWrite, // open file write-only foReadWrite // open for both ); FileShareMode = ( fsNoShare, // file cannot be shared fsShareRead, // file can be shared for reading fsShareWrite, // file can be shared for writing fsShared // file can be shared for any access ); FileCreateOption = ( fcNew, // always creates a new file fcExisting, // file must already exist fcAlways // file will be created if it doesn't // exist, else it will be opened ); FileSeekFrom = ( sfStart, // seek from start sfEnd, // seek from end sfHere // seek from current position ); // RawFile implements simple binary file with seeking & locking abilities. It // is used to implement the other file classes. RawFile = class( TObject ) private mFile : FileHandle; // windows file handle mFileName : string; // full name of file mIsOpen : boolean; // is it open? procedure Error( const msg : string ); public constructor Create; destructor Destroy; override; // Open a file, possibly creating it. See above for the various modes. procedure Open( const fname : string; omode : FileOpenMode; smode : FileShareMode; copts : FileCreateOption ); // Read nbytes from file into buffer pointed to by buf. Returns actual // number of bytes read, which may be less than nbytes. If the number // of bytes read is zero, then the end of file has been reached. function Read( buf : pointer; nbytes : integer ) : integer; // Write nbytes to file from buffer pointed to by buf. procedure Write( buf : pointer; nbytes : integer ); // Seek in a file function Seek( moveby : integer; from : FileSeekFrom ) : integer; // Return current read/write position in file function FilePosition : integer; // Perform region locking/unlocking function Lock( pos, len : integer ) : boolean; procedure Unlock( pos, len : integer ); // Close the file. It is always safe to call Close, even on an // already closed file. procedure Close; // Accessors for file name and open state property FileName : string read mFileName; property IsOpen : boolean read mIsOpen; end; // Text file buffer. This class is used solely to implement the TextFile class. TFBuffer = class( TObject ) private mBuffer : array[ 0..1023 ] of char; mPtr, mBytes : integer; public constructor Create; function Fill( f : RawFile ) : boolean; function GetLine( f : RawFile; var line : string ) : boolean; procedure Reset; function GetChar( f : RawFile; var c : char ) : boolean; end; // Text file modes TextFileOpenMode = ( toRead, // open for reading toReWrite, // open for overwrite existing contents toAppend // open for append to existing contents ); TextFileShareMode = ( smShare, // open shared (for read only) smNoShare // open single user ); // The TextFile class implements access to files consisting of lines // of text. Text files do not support seeking, and have limited open and // sharing modes (see above). TextFile = class( TObject ) private mFile : RawFile; // implemented via RawFile mBuffer : TFBuffer; // text file buffer public constructor Create; destructor Destroy; override; // Open a text file procedure Open( const fname : string; omode : TextFileOpenMode; smode : TextFileShareMode ); // Close file. Always safe to call, even on already closed files. procedure Close; // accessors for RawFile properties function FileName : string; function IsOpen : boolean; // Write a line of text to file & terminate with CR/LF pair procedure WriteLine( const line : string ); // Read a line from file, stripping CR/LF pair. Returns False if // at end of file. function ReadLine( var line : string ) : boolean; end; // This class supports random access to fixed-sized records. RandomAccessFile = class( TObject ) private mFile : RawFile; // RawFile implementation mRecSize : integer; // record size public constructor Create; destructor Destroy; override; // Open or create a RandomAccessFile. The RecSize parameter indicates // the size of the recoord in the file. This is not stored in the // file itself. procedure Open( const fname : string; recsize : integer; omode : FileOpenMode; smode : FileShareMode; copts : FileCreateOption ); // Usual stuff procedure Close; function FileName : string; function IsOpen : boolean; // write a record at record number recno, which must be greater or // equal to zero. A record number greater than that of the last // record will extend the file. procedure WriteRecord( rec : pointer; recno : integer ); // Read a record. If the record does not exist, the function returns false. function ReadRecord( rec : pointer; recno : integer ) : boolean; // read the next record sequentially. The first call to this method must // be preceded with a call to ReadRecord. function ReadNextRecord( rec : pointer ) : boolean; // Record locking function LockRecord( recno : integer ) : boolean; procedure UnlockRecord( recno : integer ); // Extend the file by count records. The new records will contain garbage. procedure Extend( count : integer ); // Return the number of recordsin the file. function RecordCount : integer; end; //------------------------------------------------------------------------------ implementation type // These declarations are necessary as there seems to be a problem with // the Borland-supplied declarations in Windows.Pas, at least in Delphi 2. LPINTEGER = ^integer; function Win32WriteFile( f : integer; p : pointer; nb : integer; nbr : LPINTEGER; junk : pointer ) : BOOL; stdcall; external kernel32 name 'WriteFile'; function Win32ReadFile( f : integer; p : pointer; nb : integer; nbr : LPINTEGER; junk : pointer ) : BOOL; stdcall; external kernel32 name 'ReadFile'; const // erroor messages FILE_OPEN_EMSG = 'Could not open file'; FILE_NOT_OPEN_EMSG = 'File is not open'; BAD_BUFFER_SIZE_EMSG = 'Bad buffer size for Read/Write'; READ_FAILED_EMSG = 'Read failed'; WRITE_FAILED_EMSG = 'Write failed'; SEEK_FAILED_EMSG = 'Seek failed'; BAD_TFSHARE_EMSG = 'Cannot open text file for write in shared mode'; BAD_LOCK_VALUES_EMSG = 'Bad range values for lock/unlock'; UNLOCK_FAILED_EMSG = 'Unlock failed!'; BAD_REC_SIZE_EMSG = 'Record size must be greater than zero'; BAD_REC_NUMBER_EMSG = 'Bad record number'; NIL_POINTER_EMSG = 'Nil pointer'; //------------------------------------------------------------------------------ // Utility stuff //------------------------------------------------------------------------------ // replace with assert in Delphi3 procedure CheckPointer( p : pointer ); begin if ( p = nil ) then raise FileError.Create( NIL_POINTER_EMSG ); end; //------------------------------------------------------------------------------ // RawFile methods //------------------------------------------------------------------------------ // Create new RawFile constructor RawFile.Create; begin mFile := 0; mIsOpen := false; mFileName := ''; end; // Destroy RawFile, closing disk image first. destructor RawFile.Destroy; begin Close; inherited Destroy; end; // RawFile error messaging procedure RawFile.Error( const msg : string ); begin raise Fileerror.CreateFmt( '%s: %s', [mFileNAme, msg ] ); end; // Close RawFile procedure RawFile.Close; begin if ( mIsOpen ) then CloseHandle( mFile ); mIsOpen := false; end; // Open RawFile. Most of this is mapping my modes onto Windows modes. Calling // this on an already open file will Close & then re-open it. procedure RawFile.Open( const fname : string; omode : FileOpenMode; smode : FileShareMode; copts : FileCreateOption ); var oflags, sflags, cflags : integer; begin Close; mFileName := fname; oflags := 0; sflags := 0; cflags := 0; if ( omode = foRead ) then oflags := GENERIC_READ else if ( omode = foWrite ) then oflags := GENERIC_WRITE else oflags := GENERIC_READ + GENERIC_WRITE; if ( smode = fsShareRead ) then sflags := FILE_SHARE_READ else if ( smode = fsShareWrite ) then sflags := FILE_SHARE_WRITE else if ( smode = fsShared ) then sflags := FILE_SHARE_WRITE + FILE_SHARE_READ; if ( copts = fcNew ) then cflags := CREATE_ALWAYS else if ( copts = fcExisting ) then cflags := OPEN_EXISTING else if ( copts = fcAlways ) then cflags := OPEN_ALWAYS; mFile := Windows.CreateFile( PChar( fname ), oflags, sflags, nil, cflags, FILE_ATTRIBUTE_NORMAL, 0 ); if ( mFile = INVALID_HANDLE_VALUE ) then begin mIsOpen := false; Error( FILE_OPEN_EMSG ); end; mIsOpen := true; end; // Read bytes from file function RawFile.Read( buf : pointer; nbytes : integer ) : integer; var bread : integer; begin CheckPointer( buf ); if ( not IsOpen ) then // must be open Error( FILE_NOT_OPEN_EMSG ); if ( nbytes <= 0 ) then // byte number must be sensible Error( BAD_BUFFER_SIZE_EMSG ); if ( Win32ReadFile( mFile, buf, nbytes, @bread, nil ) ) then result := bread else result := 0; end; // Write bytes to file procedure RawFile.Write( buf : pointer; nbytes : integer ); var bwrite : integer; begin CheckPointer( buf ); if ( not IsOpen ) then Error( FILE_NOT_OPEN_EMSG ); if ( nbytes <= 0 ) then Error( BAD_BUFFER_SIZE_EMSG ); if ( not Win32WriteFile( mFile, buf, nbytes, @bwrite, nil ) ) then Error( WRITE_FAILED_EMSG ); end; // Get current position. This involves seeking to end of file & then back // again and could therefore be slow. function RawFile.FilePosition : integer; var pos : integer; begin if ( not IsOpen ) then Error( FILE_NOT_OPEN_EMSG ); pos := SetFilePointer( mFile, 0, nil, FILE_CURRENT ); if ( pos = -1 ) then Error( SEEK_FAILED_EMSG ); result := pos; end; // Seek in file, returning new position. Raises exception if seek fails. function RawFile.Seek( moveby : integer; from : FileSeekFrom ) : integer; var mflags : integer; begin if ( not IsOpen ) then Error( FILE_NOT_OPEN_EMSG ); if ( from = sfStart ) then mflags := FILE_BEGIN else if ( from = sfEnd ) then mflags := FILE_END else mflags := FILE_CURRENT; result := SetFilePointer( mFile, moveby, nil, mflags ); if ( result = -1 ) then Error( SEEK_FAILED_EMSG ); end; // Lock a range of bytes function RawFile.Lock( pos, len : integer ) : boolean; begin if ( not IsOpen ) then Error( FILE_NOT_OPEN_EMSG ); if ( (pos < 0) or (len <= 0 ) ) then Error( BAD_LOCK_VALUES_EMSG ); result := LockFile( mFile, pos, 0, len, 0 ); end; // Unlock a range of bytesa procedure RawFile.UnLock( pos, len : integer ); begin if ( not IsOpen ) then Error( FILE_NOT_OPEN_EMSG ); if ( (pos < 0) or (len <= 0 ) ) then Error( BAD_LOCK_VALUES_EMSG ); if ( not UnLockFile( mFile, pos, 0, len, 0 ) ) then Error( UNLOCK_FAILED_EMSG ); end; //------------------------------------------------------------------------------ // TFBuffer methods. //------------------------------------------------------------------------------ constructor TFBuffer.Create; begin Reset; end; // Fill a buffer by reading raw bytes function TFBuffer.Fill( f : RawFile ) : boolean; begin mBytes := f.Read( @mBuffer, sizeof( mBuffer )) ; mPtr := 0; result := mBytes <> 0; end; // Get single character from the buffer, which will refill itself as // necessary. Returns false on EOF. function TFBuffer.GetChar( f : RawFile; var c : char ) : boolean; var t : char; begin result := false; if ( (mPtr >= mBytes) and (not Fill( f ) ) ) then // eof exit; t := mBuffer[mPtr]; inc( mPtr ); result := true; if ( t = #13 ) then begin GetChar( f, t ); c := #0; end else c := t; end; // Read line from buffer, stripping CR/LF. The buffer re-fills as necessary. function TFBuffer.GetLine( f : RawFile; var line : string ) : boolean; var c : char; begin line := ''; while( GetChar( f, c ) ) do begin if ( c = #0 ) then begin result := true; exit; end; line := line + c; end; result := Line <> ''; end; // empty the buffer procedure TFBuffer.Reset; begin mPtr := 0; mBytes := 0; end; //------------------------------------------------------------------------------ // TextFile methods. Most work is done by the RawFile and TFBuffer classes. //------------------------------------------------------------------------------ // Constructor creates the rawfile & buffer object constructor TextFile.Create; begin mFile := RawFile.Create; mBuffer := TFBuffer.Create; end; // destroy rawfile & buffer destructor TextFile.Destroy; begin mBuffer.Free; mFile.Free; inherited destroy; end; // Once again, open is mostly about mapping modes procedure TextFile.Open( const fname : string; omode : TextFileOpenMode; smode : TextFileShareMode ); var romode : FileOpenMode; rsmode : FileShareMode; rcmode : FileCreateOption; begin if ( omode = toRead ) then romode := foRead else romode := foWrite; if ( smode = smNoShare ) then rsmode := fsNoShare else if ( romode = foRead ) then rsmode := fsShareRead else raise FileError.CreateFmt( '%s: %s', [fname, BAD_TFSHARE_EMSG] ); if ( omode = toRead ) then rcmode := fcExisting else if ( omode = toReWrite ) then rcmode := fcNew else if ( omode = toAppend ) then rcmode := fcExisting; mFile.Open( fname, romode, rsmode, rcmode ); if ( omode = toAppend ) then mFile.Seek( 0, sfEnd ); end; // Close file procedure TextFile.Close; begin mFile.Close; end; // Get file name (may be empty) function TextFile.FileName : string; begin result := mFile.FileName; end; // Get open state function TextFile.IsOpen : boolean; begin result := mFile.IsOpen; end; // write line to text file, terminating with CR/LF pair procedure TextFile.WriteLine( const line : string ); const crlf : array[0..2] of char = #13#10#0; var p : pchar; begin p := pchar( line ); if ( length( line ) > 0 ) then mFile.Write( p, length( line ) ); mFile.Write( @crlf, 2 ); end; // Read line, trimming CR/LF. function TextFile.ReadLine( var line : string ) : boolean; begin result := mBuffer.GetLine( mFile, line ); end; //------------------------------------------------------------------------------ // RandomAccessFile methods. RawFile class does most of the work. //------------------------------------------------------------------------------ // constructor creates rawfile constructor RandomAccessFile.Create; begin mfile := RawFile.Create; end; destructor RandomAccessFile.Destroy; begin mFile.Free; inherited Destroy; end; // Mode mapping not necessary, as we pass things thru to RawFile procedure RandomAccessFile.Open( const fname : string; recsize : integer; omode : FileOpenMode; smode : FileShareMode; copts : FileCreateOption ); begin mFile.Open( fname, omode, smode, copts ); if ( recsize <= 0 ) then begin mFile.Close; mFile.Error( BAD_REC_SIZE_EMSG ); end; mRecSize := recsize; end; // usual stuff procedure RandomAccessFile.Close; begin mFile.close; end; function RandomAccessFile.FileName : string; begin result := mFile.FileName; end; function RandomAccessFile.IsOpen : boolean; begin result := mFile.IsOpen; end; // write record. If record number higher than current highest, the call // to Seek will extend the file procedure RandomAccessFile.WriteRecord( rec : pointer; recno : integer ); begin if ( recno < 0 ) then mFile.Error( BAD_REC_NUMBER_EMSG ); mFile.Seek( recno * mRecSize, sfStart ); mFile.Write( rec, mRecSize ); end; // read a record function RandomAccessFile.ReadRecord( rec : pointer; recno : integer ) : boolean; begin if ( recno < 0 ) then mFile.Error( BAD_REC_NUMBER_EMSG ); if ( RecordCount <= recno ) then result := false else begin mFile.Seek( recno * mRecSize, sfStart ); result := mFile.Read( rec, mRecSize ) = mRecSize; end; end; // read next record. Should have made som positioning call (like REadRecord) // before calling this function RandomAccessFile.ReadNextRecord( rec : pointer ) : boolean; begin mFile.Seek( 0, sfHere ); result := mFile.Read( rec, mRecSize ) = mRecSize; end; // Record locking function RandomAccessFile.LockRecord( recno : integer ) : boolean; begin result := mFile.Lock( recno * mRecSize, mRecSize ); end; // and unlocking procedure RandomAccessFile.UnlockRecord( recno : integer ); begin mFile.Unlock( recno * mRecSize, mRecSize ); end; // extend file by count records procedure RandomAccessFile.Extend( count : integer ); var c : char; begin if ( count > 0 ) then mFile.Seek( (count - 1 ) * mRecSize, sfEnd ); // for the last record we write a byte at its very end if ( mRecSize > 1 ) then mFile.Seek( mRecSize - 1, sfHere ); mFile.Write( @c, 1 ); // must write in order to extend end; // Return number of records. This causes several seeks, so may be slow function RandomAccessFile.RecordCount : integer; var now : integer; begin now := mFile.FilePosition; result := mFile.Seek( 0, sfEnd ) div mRecSize; mFile.Seek( now, sfStart ); end; //------------------------------------------------------------------------------ end.