(* RndFile.mod implement the ISO RndFile specification.
Copyright (C) 2008-2023 Free Software Foundation, Inc.
Contributed by Gaius Mulley <gaius.mulley@southwales.ac.uk>.
This file is part of GNU Modula-2.
GNU Modula-2 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
GNU Modula-2 is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
Under Section 7 of GPL version 3, you are granted additional
permissions described in the GCC Runtime Library Exception, version
3.1, as published by the Free Software Foundation.
You should have received a copy of the GNU General Public License and
a copy of the GCC Runtime Library Exception along with this program;
see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
<http://www.gnu.org/licenses/>.  *)
IMPLEMENTATION MODULE RndFile ;
FROM RTgen IMPORT ChanDev, DeviceType,
                  InitChanDev, doLook, doSkip, doSkipLook, doWriteLn,
                  doReadText, doWriteText, doReadLocs, doWriteLocs,
                  checkErrno ;
FROM RTfio IMPORT doreadchar, dounreadchar, dogeterrno, dorbytes,
                  dowbytes, dowriteln, iseof, iseoln, iserror ;
FROM IOLink IMPORT DeviceId, DeviceTablePtr, IsDevice, MakeChan, UnMakeChan,
                   DeviceTablePtrValue, RAISEdevException, AllocateDeviceId,
                   ResetProc ;
FROM Storage IMPORT ALLOCATE, DEALLOCATE ;
FROM RTgenif IMPORT GenDevIF, InitGenDevIF ;
FROM FIO IMPORT File ;
FROM libc IMPORT memcpy ;
FROM errno IMPORT geterrno ;
FROM IOConsts IMPORT ReadResults ;
FROM ChanConsts IMPORT readFlag, writeFlag ;
FROM EXCEPTIONS IMPORT ExceptionNumber, RAISE,
                       AllocateSource, ExceptionSource, IsCurrentSource,
                       IsExceptionalExecution ;
IMPORT FIO, SYSTEM, RTio, errno, ErrnoCategory ;
VAR
   dev             : ChanDev ;
   did             : DeviceId ;
   rndfileException: ExceptionSource ;
PROCEDURE look (d: DeviceTablePtr;
                VAR ch: CHAR; VAR r: ReadResults) ;
BEGIN
   checkRW(FALSE, d) ;
   doLook(dev, d, ch, r)
END look ;
PROCEDURE skip (d: DeviceTablePtr) ;
BEGIN
   doSkip(dev, d)
END skip ;
PROCEDURE skiplook (d: DeviceTablePtr;
                    VAR ch: CHAR; VAR r: ReadResults) ;
BEGIN
   checkRW(FALSE, d) ;
   doSkipLook(dev, d, ch, r)
END skiplook ;
PROCEDURE lnwrite (d: DeviceTablePtr) ;
BEGIN
   checkRW(TRUE, d) ;
   doWriteLn(dev, d)
END lnwrite ;
PROCEDURE textread (d: DeviceTablePtr;
                    to: SYSTEM.ADDRESS;
                    maxChars: CARDINAL;
                    VAR charsRead: CARDINAL) ;
BEGIN
   checkRW(FALSE, d) ;
   doReadText(dev, d, to, maxChars, charsRead)
END textread ;
PROCEDURE textwrite (d: DeviceTablePtr;
                     from: SYSTEM.ADDRESS;
                     charsToWrite: CARDINAL);
BEGIN
   checkRW(TRUE, d) ;
   doWriteText(dev, d, from, charsToWrite)
END textwrite ;
PROCEDURE rawread (d: DeviceTablePtr;
                   to: SYSTEM.ADDRESS;
                   maxLocs: CARDINAL;
                   VAR locsRead: CARDINAL) ;
BEGIN
   checkRW(FALSE, d) ;
   doReadLocs(dev, d, to, maxLocs, locsRead)
END rawread ;
PROCEDURE rawwrite (d: DeviceTablePtr;
                    from: SYSTEM.ADDRESS;
                    locsToWrite: CARDINAL) ;
BEGIN
   checkRW(TRUE, d) ;
   doWriteLocs(dev, d, from, locsToWrite)
END rawwrite ;
PROCEDURE getname (d: DeviceTablePtr;
                   VAR a: ARRAY OF CHAR) ;
BEGIN
   FIO.GetFileName(RTio.GetFile(d^.cid), a)
END getname ;
PROCEDURE flush (d: DeviceTablePtr) ;
BEGIN
   FIO.FlushBuffer(RTio.GetFile(d^.cid))
END flush ;
(*
   checkOpenErrno - assigns, e, and, res, depending upon file result of opening,
                    file.
*)
PROCEDURE checkOpenErrno (file: FIO.File; VAR e: INTEGER; VAR res: OpenResults) ;
BEGIN
   IF FIO.IsNoError(file)
   THEN
      e := 0 ;
   ELSE
      e := errno.geterrno()
   END ;
   res := ErrnoCategory.GetOpenResults(e)
END checkOpenErrno ;
(*
   checkRW - ensures that the file attached to, p, has been opened, towrite.
*)
PROCEDURE checkRW (towrite: BOOLEAN; p: DeviceTablePtr) ;
VAR
   pb      : POINTER TO BOOLEAN ;
   fp      : FilePos ;
   file    : File ;
   name    : SYSTEM.ADDRESS ;
   size    : CARDINAL ;
   contents: SYSTEM.ADDRESS ;
BEGIN
   pb := p^.cd ;
   IF pb^#towrite
   THEN
      WITH p^ DO
         pb^ := towrite ;
         fp := CurrentPos(cid) ;
         file := RTio.GetFile(RTio.ChanId(cid)) ;
         name := FIO.getFileName(file) ;
         size := FIO.getFileNameLength(file) ;
         ALLOCATE(contents, size+1) ;
         contents := memcpy(contents, name, size) ;
         FIO.Close(file) ;
         file := FIO.openForRandom(contents, size, towrite, FALSE) ;
         RTio.SetFile(cid, file) ;
         SetPos(cid, fp) ;
         DEALLOCATE(contents, size+1)
      END
   END
END checkRW ;
(*
   newCid - returns a ChanId which represents the opened file, name.
            res is set appropriately on return.
*)
PROCEDURE newCid (fname: ARRAY OF CHAR;
                  f: FlagSet;
                  VAR res: OpenResults;
                  toWrite, newfile: BOOLEAN;
                  whichreset: ResetProc) : ChanId ;
VAR
   c   : RTio.ChanId ;
   file: FIO.File ;
   e   : INTEGER ;
   p   : DeviceTablePtr ;
   pb  : POINTER TO BOOLEAN ;
BEGIN
   file := FIO.OpenForRandom(fname, toWrite, newfile) ;
   checkOpenErrno(file, e, res) ;
   IF FIO.IsNoError(file)
   THEN
      NEW(pb) ;
      pb^ := toWrite ;
      MakeChan(did, c) ;
      RTio.SetFile(c, file) ;
      p := DeviceTablePtrValue(c, did) ;
      WITH p^ DO
         cd := pb ;
         flags := f ;
         errNum := e ;
         doLook := look ;
         doSkip := skip ;
         doSkipLook := skiplook ;
         doLnWrite := lnwrite ;
         doTextRead := textread ;
         doTextWrite := textwrite ;
         doRawRead := rawread ;
         doRawWrite := rawwrite ;
         doGetName := getname ;
         doReset := whichreset ;
         doFlush := flush ;
         doFree := handlefree
      END ;
      RETURN( c )
   ELSE
      RETURN( IOChan.InvalidChan() )
   END
END newCid ;
(*
   handlefree -
*)
PROCEDURE handlefree (d: DeviceTablePtr) ;
VAR
   f : File ;
   pb: POINTER TO BOOLEAN ;
BEGIN
   WITH d^ DO
      doFlush(d) ;
      checkErrno(dev, d) ;
      f := RTio.GetFile(RTio.ChanId(cid)) ;
      IF FIO.IsNoError(f)
      THEN
         FIO.Close(f) ;
      END ;
      checkErrno(dev, d) ;
      pb := cd ;
      DISPOSE(pb) ;
      cd := NIL
   END
END handlefree ;
PROCEDURE resetRandom (d: DeviceTablePtr) ;
BEGIN
   WITH d^ DO
      IF IsRndFile(cid)
      THEN
         (* --fixme --, finish this *)
      ELSE
         RAISEdevException(cid, did, IOChan.wrongDevice,
                           'RndFile.' + __FUNCTION__ +
                           ': channel is not a random file')
      END
   END
END resetRandom ;
PROCEDURE OpenOld (VAR cid: ChanId; name: ARRAY OF CHAR; flags: FlagSet;
                   VAR res: OpenResults);
  (* Attempts to obtain and open a channel connected to a stored random
     access file of the given name.
     The old flag is implied; without the write flag, read is implied;
     without the text flag, raw is implied.
     If successful, assigns to cid the identity of the opened channel,
     assigns the value opened to res, and sets the read/write position
     to the start of the file.
     If a channel cannot be opened as required, the value of res indicates
     the reason, and cid identifies the invalid channel.
  *)
BEGIN
   INCL(flags, ChanConsts.oldFlag) ;
   IF NOT (ChanConsts.writeFlag IN flags)
   THEN
      INCL(flags, ChanConsts.readFlag)
   END ;
   IF NOT (ChanConsts.textFlag IN flags)
   THEN
      INCL(flags, ChanConsts.rawFlag)
   END ;
   cid := newCid(name, flags, res, FALSE, FALSE, resetRandom)
END OpenOld ;
PROCEDURE OpenClean (VAR cid: ChanId; name: ARRAY OF CHAR; flags: FlagSet;
                     VAR res: OpenResults);
  (* Attempts to obtain and open a channel connected to a stored random
     access file of the given name.
     The write flag is implied; without the text flag, raw is implied.
     If successful, assigns to cid the identity of the opened channel,
     assigns the value opened to res, and truncates the file to zero length.
     If a channel cannot be opened as required, the value of res indicates
     the reason, and cid identifies the invalid channel.
  *)
BEGIN
   INCL(flags, ChanConsts.writeFlag) ;
   IF NOT (ChanConsts.textFlag IN flags)
   THEN
      INCL(flags, ChanConsts.rawFlag)
   END ;
   cid := newCid(name, flags, res, TRUE, TRUE, resetRandom)
END OpenClean ;
PROCEDURE IsRndFile (cid: ChanId): BOOLEAN;
  (* Tests if the channel identified by cid is open to a random access file. *)
BEGIN
   RETURN( (cid # NIL) AND (IOChan.InvalidChan() # cid) AND
           (IsDevice(cid, did)) AND
           ((ChanConsts.readFlag IN IOChan.CurrentFlags(cid)) OR
            (ChanConsts.writeFlag IN IOChan.CurrentFlags(cid))) )
END IsRndFile ;
PROCEDURE IsRndFileException (): BOOLEAN;
  (* Returns TRUE if the current coroutine is in the exceptional execution
     state because of the raising of a RndFile exception; otherwise returns
     FALSE.
  *)
BEGIN
   RETURN( IsCurrentSource (rndfileException) )
END IsRndFileException ;
PROCEDURE StartPos (cid: ChanId): FilePos;
  (* If the channel identified by cid is not open to a random access file,
     the exception wrongDevice is raised; otherwise returns the position of
     the start of the file.
  *)
VAR
   d: DeviceTablePtr ;
BEGIN
   IF IsRndFile(cid)
   THEN
      d := DeviceTablePtrValue(cid, did) ;
      RETURN( 0 )
   ELSE
      RAISEdevException(cid, did, IOChan.wrongDevice,
                        'RndFile.' + __FUNCTION__ +
                        ': channel is not a random file')
   END
END StartPos ;
PROCEDURE CurrentPos (cid: ChanId): FilePos;
  (* If the channel identified by cid is not open to a random access file,
     the exception wrongDevice is raised; otherwise returns the position
     of the current read/write position.
  *)
VAR
   d: DeviceTablePtr ;
BEGIN
   IF IsRndFile(cid)
   THEN
      d := DeviceTablePtrValue(cid, did) ;
      WITH d^ DO
         RETURN( FIO.FindPosition(RTio.GetFile(cid)) )
      END
   ELSE
      RAISEdevException(cid, did, IOChan.wrongDevice,
                        'RndFile.' + __FUNCTION__ +
                        ': channel is not a random file')
   END
END CurrentPos ;
PROCEDURE EndPos (cid: ChanId): FilePos;
  (* If the channel identified by cid is not open to a random access file,
     the exception wrongDevice is raised; otherwise returns the first
     position after which there have been no writes.
  *)
VAR
   d  : DeviceTablePtr ;
   end,
   old: FilePos ;
BEGIN
   IF IsRndFile(cid)
   THEN
      d := DeviceTablePtrValue(cid, did) ;
      old := CurrentPos(cid) ;
      WITH d^ DO
         old := CurrentPos(cid) ;
         FIO.SetPositionFromEnd(RTio.GetFile(cid), 0) ;
         checkErrno(dev, d) ;
         end := CurrentPos(cid) ;
         FIO.SetPositionFromBeginning(RTio.GetFile(cid), old) ;
         RETURN( end )
      END
   ELSE
      RAISEdevException(cid, did, IOChan.wrongDevice,
                        'RndFile.' + __FUNCTION__ +
                        ': channel is not a random file')
   END
END EndPos ;
PROCEDURE NewPos (cid: ChanId; chunks: INTEGER; chunkSize: CARDINAL;
                  from: FilePos): FilePos;
  (* If the channel identified by cid is not open to a random access file,
     the exception wrongDevice is raised; otherwise returns the position
     (chunks * chunkSize) relative to the position given by from, or
     raises the exception posRange if the required position cannot be
     represented as a value of type FilePos.
  *)
VAR
   d: DeviceTablePtr ;
BEGIN
   IF IsRndFile(cid)
   THEN
      d := DeviceTablePtrValue(cid, did) ;
      WITH d^ DO
         RETURN( from+VAL(FilePos, chunks*VAL(INTEGER, chunkSize))-
                 CurrentPos(cid) )
      END
   ELSE
      RAISEdevException(cid, did, IOChan.wrongDevice,
                        'RndFile.' + __FUNCTION__ +
                        ': channel is not a random file')
   END
END NewPos ;
PROCEDURE SetPos (cid: ChanId; pos: FilePos);
  (* If the channel identified by cid is not open to a random access file,
     the exception wrongDevice is raised; otherwise sets the read/write
     position to the value given by pos.
  *)
VAR
   d: DeviceTablePtr ;
BEGIN
   IF IsRndFile(cid)
   THEN
      d := DeviceTablePtrValue(cid, did) ;
      WITH d^ DO
         FIO.SetPositionFromBeginning(RTio.GetFile(cid), pos) ;
         checkErrno(dev, d)
      END
   ELSE
      RAISEdevException(cid, did, IOChan.wrongDevice,
                        'RndFile.' + __FUNCTION__ +
                        ': channel is not a random file')
   END
END SetPos ;
PROCEDURE Close (VAR cid: ChanId);
  (* If the channel identified by cid is not open to a random access file,
     the exception wrongDevice is raised; otherwise closes the channel,
     and assigns the value identifying the invalid channel to cid.
  *)
BEGIN
   IF IsRndFile(cid)
   THEN
      UnMakeChan(did, cid) ;
      cid := IOChan.InvalidChan()
   ELSE
      RAISEdevException(cid, did, IOChan.wrongDevice,
                        'RndFile.' + __FUNCTION__ +
                        ': channel is not a random file')
   END
END Close ;
(*
   Init -
*)
PROCEDURE Init ;
VAR
   gen: GenDevIF ;
BEGIN
   AllocateDeviceId(did) ;
   gen := InitGenDevIF(did, doreadchar, dounreadchar,
                       dogeterrno, dorbytes, dowbytes,
                       dowriteln,
                       iseof, iseoln, iserror) ;
   dev := InitChanDev(streamfile, did, gen) ;
   AllocateSource (rndfileException)
END Init ;
BEGIN
   Init
END RndFile.