-- FTPAltoFile.mesa,  Edit:
  -- MAS Apr 21, 1980 5:37 PM  
  -- HGM July 28, 1980  11:20 PM  
  -- PLK May 22, 1980  4:38 PM  

-- Copyright  Xerox Corporation 1979, 1980

DIRECTORY
  AltoDefs USING [BytesPerPage, BytesPerWord, PageSize],
  AltoFileDefs USING [eofDA, FA, FP, LD, TIME],
  DiskKDDefs USING [DiskFull],
  FTPDefs,
  FTPPrivateDefs,
  Inline: FROM "Inline" USING [BITAND, DIVMOD, LongMult],
  Mopcodes USING [zPOP, zEXCH],
  Process USING [Yield],
  SegmentDefs USING [
    AccessOptions, Append, Read, Write,
    VersionOptions, DefaultVersion, OldFileOnly,
    FileError, FileNameError, GetEndOfFile, GetFileTimes,
    FileHandle, DestroyFile, InsertFile, InsertFileLength, NewFile,
    ReleaseFile, FileSegmentHandle, NewFileSegment, FileSegmentAddress,
    DeleteFileSegment, SwapIn, SwapOut, Unlock, UnlockFile],
  StreamDefs USING [
    CreateByteStream, DiskHandle, FileLength, GetIndex,
    ReadBlock, SetIndex, StreamIndex, WriteBlock, TruncateDiskStream],
  String USING [AppendChar, AppendLongNumber, AppendString, EquivalentString],
  Storage USING [Node, Free, PagesForWords, Pages, FreePages],
  Time USING [Append, Unpack],
  TimeExtraDefs USING [PackedTimeFromString],
  DirExtraDefs USING [EnumerateDirectoryMasked];

FTPAltoFile: MONITOR
  -- Note:  UniqueFileTag constitutes the monitor.
    IMPORTS
      DiskKDDefs, Inline, Process,
      SegmentDefs, StreamDefs, String, Storage, Time,
      TimeExtraDefs, DirExtraDefs,
      FTPPrivateDefs
    EXPORTS FTPDefs
    SHARES FTPDefs, FTPPrivateDefs =
BEGIN OPEN FTPDefs, FTPPrivateDefs; 

-- **********************!  Types  !***********************

-- alto file system state information
AltoFileSystem: TYPE = POINTER TO AltoFileSystemObject;
AltoFileSystemObject: TYPE = RECORD [
  bufferSize: CARDINAL];

-- alto file handle state information
AltoFileHandle: TYPE = POINTER TO AltoFileHandleObject;
AltoFileHandleObject: TYPE = RECORD [
  mode: Mode,
  diskHandle: StreamDefs.DiskHandle,
  lengthOfFile: LONG INTEGER];

-- **********************!  Constants  !***********************

defaultBufferSize:		CARDINAL = 4*AltoDefs.PageSize;
scanByteCountBeforeYield:	CARDINAL = 5*256*2;

filenameWildString:		CHARACTER = '*;
filenameWildCharacter:		CHARACTER = '#;
filenameNameVersionSeparator:	CHARACTER = '!;

ftpsystem: POINTER TO FTPSystem = LocateFtpSystemObject[];

filePrimitivesObject: FilePrimitivesObject ← [
  CreateFileSystem: CreateFileSystem,
  DestroyFileSystem: DestroyFileSystem,
  DecomposeFilename: DecomposeFilename,
  ComposeFilename: ComposeFilename,
  InspectCredentials: InspectCredentials,
  EnumerateFiles: EnumerateFiles,
  OpenFile: OpenFile,
  ReadFile: ReadFile,
  WriteFile: WriteFile,
  CloseFile: CloseFile,
  DeleteFile: DeleteFile,
  RenameFile: RenameFile];

-- **********************!  Variables  !***********************

uniqueFileTag: LONG INTEGER ← 0;

-- **********************!  File Foothold Procedure  !***********************

AltoFilePrimitives,
SomeFilePrimitives: PUBLIC PROCEDURE RETURNS [filePrimitives: FilePrimitives] =
  BEGIN
  -- return file primitives
    filePrimitives ← @filePrimitivesObject;
  END;

-- **********************!  File Primitives  !***********************

CreateFileSystem: PROCEDURE [bufferSize: CARDINAL] RETURNS [fileSystem: FileSystem] =
  BEGIN
  -- Note:  bufferSize expressed in pages; zero implies default.
  -- local variables
    altoFileSystem: AltoFileSystem;
  -- allocate and initialize file system object
    altoFileSystem ← Storage.Node[SIZE[AltoFileSystemObject]];
    altoFileSystem↑ ← AltoFileSystemObject[
      bufferSize: IF bufferSize # 0 THEN bufferSize*AltoDefs.PageSize ELSE defaultBufferSize];
    fileSystem ← LOOPHOLE[altoFileSystem];
  END;

DestroyFileSystem: PROCEDURE [fileSystem: FileSystem] =
  BEGIN
  -- local constants
    altoFileSystem: AltoFileSystem = LOOPHOLE[fileSystem];
  -- release file system object
    Storage.Free[altoFileSystem];
  END;

DecomposeFilename: PROCEDURE [fileSystem: FileSystem, absoluteFilename: STRING, virtualFilename: VirtualFilename] =
  BEGIN OPEN virtualFilename;
  -- Note:  Absolute filenames have the following syntax,
  --   with name non-empty and version non-empty and numeric:
  --     name [filenameNameVersionSeparator version]
  --   Virtual filename components are never NIL;
  --   supplies empty device and directory components.
  -- local variables
    i: CARDINAL;
    character: CHARACTER;
    field: STRING ← name;
  -- initialize virtual filename components to empty
    device.length ← directory.length ← name.length ← version.length ← 0;
  -- process each character in absolute filename
    FOR i IN [0..absoluteFilename.length) DO
      -- select character
	character ← absoluteFilename[i];
      -- switch to version if character is name-version separator
	IF field = name AND character = filenameNameVersionSeparator THEN
	  field ← version
      -- append character to name or version as appropriate
	ELSE
	  BEGIN
	  IF field = version AND character ~IN ['0..'9] THEN Abort[illegalFilename];
	  String.AppendChar[field, character];
	  END;
      ENDLOOP;
  -- abort if either name or version is empty
    IF name.length = 0 OR (field = version AND version.length = 0) THEN
      Abort[illegalFilename];
  END;

ComposeFilename: PROCEDURE [fileSystem: FileSystem, absoluteFilename: STRING, virtualFilename: VirtualFilename] =
  BEGIN OPEN virtualFilename;
  -- Note:  Absolute filenames have the following syntax,
  --   with name non-empty and version non-empty and numeric:
  --     name [filenameNameVersionSeparator version]
  --   Virtual filename components are never NIL;
  --   ignores device and directory components;
  --   uses name and version components as defaults.
  -- local constants
    explicitDevice:    STRING = [0];
    explicitDirectory: STRING = [0];
  -- local variables
    explicitName:   STRING ← [maxStringLength];
    explicitVersion: STRING ← [maxStringLength];
    explicitVirtualFilenameObject: VirtualFilenameObject ← [
      device: explicitDevice, directory: explicitDirectory,
      name: explicitName, version: explicitVersion];
    i: CARDINAL;
  -- return at once if absolute filename is all there is
    IF name.length = 0 AND version.length = 0 THEN RETURN;
  -- decompose absolute filename
    IF absoluteFilename.length # 0 THEN
      DecomposeFilename[fileSystem, absoluteFilename, @explicitVirtualFilenameObject];
  -- apply defaults as necessary
    IF explicitName.length = 0 THEN explicitName ← name;
    IF explicitVersion.length = 0 THEN explicitVersion ← version;
  -- initialize absolute filename to empty
    absoluteFilename.length ← 0;
  -- output name always
    IF explicitName.length = 0 THEN Abort[illegalFilename];
    String.AppendString[absoluteFilename, explicitName];
  -- output version if specified
    IF explicitVersion.length # 0 THEN
      BEGIN
      -- verify that version is numeric
	FOR i IN [0..explicitVersion.length) DO
	  IF explicitVersion[i] ~IN ['0..'9] THEN Abort[illegalFilename];
	  ENDLOOP;
      -- output name-version separator
	String.AppendChar[absoluteFilename, filenameNameVersionSeparator];
      -- output version
	String.AppendString[absoluteFilename, explicitVersion];
      END;
  END;


InspectCredentials: PROCEDURE [fileSystem: FileSystem, status: Status, user, password: STRING] =
  BEGIN
  -- no operation
  END;

EnumerateFiles: PROCEDURE [fileSystem: FileSystem, files: STRING, intent: EnumerateFilesIntent, processFile: PROCEDURE 
[UNSPECIFIED, STRING, FileInfo], processFileData: UNSPECIFIED] =
  BEGIN
    PreProcessFile: PROCEDURE [fp: POINTER TO AltoFileDefs.FP, file: STRING]
	RETURNS [BOOLEAN] =
      BEGIN
      -- terminating period has been flushed by EnumerateDirectoryMasked
      fileHandle: SegmentDefs.FileHandle;
      read, write, create: LONG CARDINAL;
      page, byte: CARDINAL;
      fa: AltoFileDefs.FA ← [AltoFileDefs.eofDA, 0, 0];
      fileHandle ← SegmentDefs.InsertFile[fp, SegmentDefs.Read];
      SegmentDefs.InsertFileLength[fileHandle, @fa];
      creationDate.length ← writeDate.length ← readDate.length ← 0;
      [read, write, create] ← SegmentDefs.GetFileTimes[fileHandle];
      Time.Append[creationDate,Time.Unpack[create]];
      Time.Append[writeDate,Time.Unpack[write]];
      Time.Append[readDate,Time.Unpack[read]];
      [page, byte] ← SegmentDefs.GetEndOfFile[fileHandle];
      fileInfoObject.byteCount ← Inline.LongMult[page, 512] + byte - 512;
      IF fileHandle.segcount = 0 THEN SegmentDefs.ReleaseFile[fileHandle];
      fileInfoObject.fileType ← unknown;
      fileInfoObject.byteSize ← 8;
      processFile[processFileData, file, @fileInfoObject];
      RETURN[FALSE];
      END;
    creationDate: STRING = [maxDateLength];
    writeDate: STRING = [maxDateLength];
    readDate: STRING = [maxDateLength];
    fileInfoObject: FileInfoObject ← [
      fileType: , byteSize: , byteCount: ,
      creationDate: creationDate, writeDate: writeDate, readDate: readDate,
      author: NIL];
    DirExtraDefs.EnumerateDirectoryMasked[files,PreProcessFile]
  END;

OpenFile: PROCEDURE [fileSystem: FileSystem, file: STRING, mode: Mode, fileTypePlease: BOOLEAN, info: FileInfo] RETURNS 
[fileHandle: FileHandle, fileType: FileType] =
  BEGIN
  -- Note:  Supplies scratch filename if file.length=0;
  --   determines file type by looking for byte with high-order bit on.
  -- local constants
    version: SegmentDefs.VersionOptions = IF mode=read OR mode=readThenWrite
      THEN SegmentDefs.OldFileOnly ELSE SegmentDefs.DefaultVersion;
    access: SegmentDefs.AccessOptions = SELECT mode FROM
      read => SegmentDefs.Read,
      write => SegmentDefs.Write + SegmentDefs.Append,
      writeThenRead, readThenWrite =>
	SegmentDefs.Read + SegmentDefs.Write + SegmentDefs.Append,
      ENDCASE => SegmentDefs.Append; -- append
    scratch: BOOLEAN = (file.length=0);
  -- local variables
    altoFileHandle: AltoFileHandle;
    internalFileHandle: SegmentDefs.FileHandle ← NIL;
    initialStreamIndex: StreamDefs.StreamIndex;
    byteCount: CARDINAL;
    byte: Byte;
  -- generate unique scratch filename if necessary
    IF scratch THEN
      BEGIN
      String.AppendString[file, "FTPAltoFile-"L];
      String.AppendLongNumber[file, UniqueFileTag[], 10];
      String.AppendString[file, ".Scratch"L];
      END;
  -- allocate and initialize alto file handle object
    altoFileHandle ← Storage.Node[SIZE[AltoFileHandleObject]];
    altoFileHandle↑ ← AltoFileHandleObject[
      mode: mode,
      diskHandle: NIL,
      lengthOfFile: ];
    fileHandle ← LOOPHOLE[altoFileHandle];
  -- intercept errors
    BEGIN OPEN altoFileHandle; ENABLE
      BEGIN
      SegmentDefs.FileNameError => Abort[
	IF version = SegmentDefs.OldFileOnly THEN noSuchFile ELSE illegalFilename];
      DiskKDDefs.DiskFull           => Abort[noRoomForFile];
      SegmentDefs.FileError        => Abort[fileDataError];
      UNWIND =>
	BEGIN
	IF diskHandle # NIL THEN diskHandle.destroy[diskHandle];
	IF internalFileHandle # NIL THEN SegmentDefs.ReleaseFile[internalFileHandle];
	Storage.Free[altoFileHandle];
	END;
      END;
  -- create byte stream to access file
    internalFileHandle ← SegmentDefs.NewFile[file, access, version];
    altoFileHandle.diskHandle ← StreamDefs.CreateByteStream[internalFileHandle, access];
    IF ~scratch AND info#NIL THEN
  -- Stuff/Extract create date (and such) to/from Leader page
      BEGIN  -- No catch phrase to fixup leaderPage
      access: SegmentDefs.AccessOptions;
      leaderPage: SegmentDefs.FileSegmentHandle;
      leader: POINTER TO AltoFileDefs.LD;
      access ← IF mode=read THEN SegmentDefs.Read ELSE SegmentDefs.Write;
      leaderPage ← SegmentDefs.NewFileSegment[internalFileHandle,0,1,access];
      SegmentDefs.SwapIn[leaderPage];
      leader ← SegmentDefs.FileSegmentAddress[leaderPage];
      SELECT mode FROM
	write =>
	  BEGIN
	  SetTime[info.creationDate,@leader.created];
	  SegmentDefs.Unlock[leaderPage];
	  SegmentDefs.SwapOut[leaderPage];
	  END;
	ENDCASE =>
	  BEGIN
	  GetTime[info.creationDate,leader.created];
	  GetTime[info.writeDate,leader.written];
	  GetTime[info.readDate,leader.read];
	  SegmentDefs.Unlock[leaderPage];
	  END;
      SegmentDefs.DeleteFileSegment[leaderPage];
      leaderPage ← NIL;
      END;
    internalFileHandle ← NIL;
  -- determine file type if necessary
    IF fileTypePlease THEN
      BEGIN
      -- save initial stream position
	initialStreamIndex ← StreamDefs.GetIndex[diskHandle];
      -- scan file until byte with higher-order bit encountered
	fileType ← text;  byteCount ← 0;
	StreamDefs.SetIndex[diskHandle, [0, 0]];
	UNTIL fileType = binary OR diskHandle.endof[diskHandle] DO
	  -- read next byte
	    byte ← diskHandle.get[diskHandle];
	  -- tally byte and yield to scheduler periodically
	    byteCount ← byteCount + 1;
	    IF byteCount >= scanByteCountBeforeYield THEN
	      BEGIN
	      byteCount ← 0;
	      Process.Yield[];
	      END;
	  -- force scan termination if byte proves file binary
	    IF byte > 177B THEN fileType ← binary;
	  ENDLOOP;
      -- restore initial stream position
	StreamDefs.SetIndex[diskHandle, initialStreamIndex];
      END
  -- leave file type unknown
    ELSE fileType ← unknown;
    END; -- enable
  END;

Flop: PROCEDURE [AltoFileDefs.TIME] RETURNS [LONG CARDINAL] = MACHINE CODE
  BEGIN
  Mopcodes.zEXCH;
  END;

Flip: PROCEDURE [LONG CARDINAL] RETURNS [AltoFileDefs.TIME] = MACHINE CODE
  BEGIN
  Mopcodes.zEXCH;
  END;

GetTime: PROCEDURE [s: STRING, t: AltoFileDefs.TIME] =
  BEGIN
  when: LONG CARDINAL ← Flop[t];
  IF s=NIL OR s.length#0 THEN RETURN;
  Time.Append[s,Time.Unpack[when]];
  END;

SetTime: PROCEDURE [s: STRING, t: POINTER TO AltoFileDefs.TIME] =
  BEGIN
  when: LONG CARDINAL;
  IF s=NIL OR s.length=0 THEN RETURN;
  when ← TimeExtraDefs.PackedTimeFromString[s];
  IF when#0 THEN t↑ ← Flip[when];
  END;

ReadFile: PROCEDURE [fileSystem: FileSystem, fileHandle: FileHandle, sendBlock: PROCEDURE [UNSPECIFIED, POINTER, CARDINAL], 
sendBlockData: UNSPECIFIED] =
  BEGIN
  -- Note:  Assumes invocation is consistent with mode declared via OpenFile;
  --   no attempt is made to double-buffer because
  --   the Alto file system monopolizes the processor.
  -- Shorten procedure
    Shorten: PROCEDURE [LONG INTEGER] RETURNS [CARDINAL] = MACHINE CODE
      BEGIN
      Mopcodes.zPOP;
      END;
  -- local constants
    altoFileSystem: AltoFileSystem = LOOPHOLE[fileSystem];
    altoFileHandle: AltoFileHandle = LOOPHOLE[fileHandle];
    readFileBufferSize: CARDINAL = altoFileSystem.bufferSize;
  -- local variables
    streamIndex: StreamDefs.StreamIndex;
    buffer: POINTER;  bufferSize: CARDINAL;
    outgoingByteCount, shortLengthToGo: CARDINAL;
    longLengthToGo: LONG INTEGER;
    lengthOfRead: LONG INTEGER ← 0;
    endOfFile: BOOLEAN ← FALSE;
  -- reset stream position
    BEGIN OPEN altoFileHandle;
    StreamDefs.SetIndex[diskHandle, [0, 0]];
  -- allocate a buffer
    buffer ← Storage.Pages[Storage.PagesForWords[readFileBufferSize]];
  -- process each outgoing block of file
    UNTIL endOfFile DO
      ENABLE UNWIND => Storage.FreePages[buffer];
      -- fill buffer
	longLengthToGo ← lengthOfFile - lengthOfRead;
	shortLengthToGo ← IF longLengthToGo > LAST[INTEGER]
	  THEN LAST[INTEGER] ELSE Shorten[longLengthToGo];
	bufferSize ←
	  IF mode # writeThenRead THEN readFileBufferSize
	  ELSE MIN[readFileBufferSize,
	    (shortLengthToGo+AltoDefs.BytesPerWord-1) / AltoDefs.BytesPerWord];
	outgoingByteCount ← AltoDefs.BytesPerWord*StreamDefs.ReadBlock[
	  diskHandle, buffer, bufferSize];
	endOfFile ←
	  IF mode # writeThenRead THEN diskHandle.endof[diskHandle]
	  ELSE lengthOfRead+outgoingByteCount >= lengthOfFile;
      -- discard excess byte if any
	IF endOfFile THEN
	  IF mode # writeThenRead THEN
	    BEGIN
	    streamIndex ← StreamDefs.FileLength[diskHandle];
	    IF Inline.BITAND[streamIndex.byte, 1] = 1 THEN
	      outgoingByteCount ← outgoingByteCount - 1;
	    END
	  ELSE
	    IF lengthOfRead+outgoingByteCount > lengthOfFile THEN
	      outgoingByteCount ← outgoingByteCount - 1;
      -- empty buffer
	IF outgoingByteCount > 0 THEN
	  sendBlock[sendBlockData, buffer, outgoingByteCount];
      -- increment length of read
	lengthOfRead ← lengthOfRead + outgoingByteCount;
      ENDLOOP;
  -- signal end of file
    sendBlock[sendBlockData, buffer, 0];
  -- release buffer
    Storage.FreePages[buffer];
    END; -- open
  END;
    
WriteFile: PROCEDURE [fileSystem: FileSystem, fileHandle: FileHandle, receiveBlock: PROCEDURE [UNSPECIFIED, POINTER, 
CARDINAL] RETURNS [CARDINAL], receiveBlockData: UNSPECIFIED] =
  BEGIN
  -- Note:  Assumes invocation is consistent with mode declared via OpenFile;
  --   no attempt is made to double-buffer because
  --   the Alto file system monopolizes the processor.
  -- local constants
    altoFileSystem: AltoFileSystem = LOOPHOLE[fileSystem];
    altoFileHandle: AltoFileHandle = LOOPHOLE[fileHandle];
    writeFileBufferSize: CARDINAL = altoFileSystem.bufferSize;
  -- local variables
    streamIndex: StreamDefs.StreamIndex;
    streamPositionEven: BOOLEAN;
    buffer: POINTER;  bufferSize: CARDINAL;
    endOfFile: BOOLEAN ← FALSE;
    incomingByteCount, outgoingByteCount: CARDINAL;
    excessByteCount, i: CARDINAL;
    word: Word;
  -- reset if necessary and note stream position
    BEGIN OPEN altoFileHandle;
    IF mode # append THEN StreamDefs.SetIndex[diskHandle, streamIndex ← [0, 0]]
    ELSE streamIndex ← StreamDefs.GetIndex[diskHandle];
    lengthOfFile ← streamIndex.page*AltoDefs.BytesPerPage + streamIndex.byte;
    streamPositionEven ← Inline.BITAND[streamIndex.byte, 1] = 0;
  -- allocate a buffer
    buffer ← Storage.Pages[Storage.PagesForWords[writeFileBufferSize]];
  -- process each incoming block of file
    UNTIL endOfFile DO
      ENABLE
	BEGIN
	DiskKDDefs.DiskFull=> Abort[noRoomForFile];
	UNWIND => Storage.FreePages[buffer];
	END;
      -- fill buffer
	outgoingByteCount ← bufferSize ← 0;
	DO
	  -- receive block
	    incomingByteCount ← receiveBlock[receiveBlockData,
	      buffer + bufferSize, writeFileBufferSize - bufferSize];
	    endOfFile ← incomingByteCount = 0;
	  -- update outgoing byte count
	    outgoingByteCount ← outgoingByteCount + incomingByteCount;
	    [bufferSize, excessByteCount] ←
	      Inline.DIVMOD[outgoingByteCount, AltoDefs.BytesPerWord];
	  -- terminate input if necessary
	    IF endOfFile OR bufferSize=writeFileBufferSize OR excessByteCount=1 THEN EXIT;
	  ENDLOOP;
      -- yield to scheduler
	Process.Yield[];
      -- empty buffer (except possible last odd byte of file)
	IF bufferSize > 0 THEN
	  IF streamPositionEven THEN          
	    [] ← StreamDefs.WriteBlock[diskHandle, buffer, bufferSize]
	  ELSE
	    BEGIN
	    word ← buffer;
	    FOR i IN [0..bufferSize) DO
	      diskHandle.put[diskHandle, word.lhByte];
	      diskHandle.put[diskHandle, word.rhByte];
	      word ← word + 1;
	      ENDLOOP;
	    END;
      -- empty buffer of last odd byte if any
	IF excessByteCount = 1 THEN
	  BEGIN
	  word ← buffer + bufferSize;
	  diskHandle.put[diskHandle, word.lhByte];
	  streamPositionEven ← ~streamPositionEven;
	  END;
      -- increment file length
	lengthOfFile ← lengthOfFile + outgoingByteCount;
      ENDLOOP;
  -- release buffer
    Storage.FreePages[buffer];
    END; -- open
  END;

CloseFile: PROCEDURE [fileSystem: FileSystem, fileHandle: FileHandle, aborted: BOOLEAN] =
  BEGIN
  -- Note:  On abort, deletes file opened for write, writeThenRead, or readThenWrite.
  -- local constants
    altoFileHandle: AltoFileHandle = LOOPHOLE[fileHandle];
  -- local variables
    fp: AltoFileDefs.FP ← altoFileHandle.diskHandle.file.fp;
    internalFileHandle: SegmentDefs.FileHandle ← NIL;
  -- intercept errors
    BEGIN OPEN altoFileHandle; ENABLE
      BEGIN
      DiskKDDefs.DiskFull    => Abort[noRoomForFile];
      SegmentDefs.FileError => Abort[fileDataError];
      UNWIND => IF internalFileHandle # NIL THEN SegmentDefs.ReleaseFile[internalFileHandle];
      END;
  -- destroy byte stream
    StreamDefs.TruncateDiskStream[diskHandle !
      DiskKDDefs.DiskFull => IF aborted THEN 
      {SegmentDefs.UnlockFile[diskHandle.file]; CONTINUE}];
  -- delete file if appropriate
    IF aborted AND (mode=write OR mode=writeThenRead OR mode=readThenWrite) THEN
      BEGIN
      internalFileHandle ← SegmentDefs.InsertFile[@fp, SegmentDefs.Write];
      SegmentDefs.DestroyFile[internalFileHandle ! SegmentDefs.FileError =>
	IF internalFileHandle.segcount # 0 OR internalFileHandle.lock # 0
	  THEN CONTINUE ELSE Abort[fileDataError]];
      END;
  -- release alto file handle object
    Storage.Free[altoFileHandle];
    END; -- enable
  END;

DeleteFile: PROCEDURE [fileSystem: FileSystem, file: STRING] =
  BEGIN
  -- local variables
    internalFileHandle: SegmentDefs.FileHandle ← NIL;
  -- intercept errors
    BEGIN ENABLE
      BEGIN
      SegmentDefs.FileNameError => Abort[noSuchFile];
      SegmentDefs.FileError       => Abort[fileDataError];
      UNWIND => IF internalFileHandle # NIL THEN SegmentDefs.ReleaseFile[internalFileHandle];
      END;
  -- delete file
    internalFileHandle ← SegmentDefs.NewFile[file, SegmentDefs.Write, SegmentDefs.OldFileOnly];
    SegmentDefs.DestroyFile[internalFileHandle];
    END; -- enable
  END;

RenameFile: PROCEDURE [fileSystem: FileSystem, currentFile, newFile: STRING] =
  BEGIN
  -- local variables
    create: STRING = [maxDateLength];
    info: FileInfoObject ← [binary,8,0,create,NIL,NIL,NIL];
    currentFileHandle, newFileHandle, temp: FileHandle ← NIL;
  -- no operation if two filenames equivalent
    IF String.EquivalentString[currentFile, newFile] THEN RETURN;
  -- open current and new files
    [currentFileHandle, ] ← OpenFile[fileSystem, currentFile, read, FALSE, @info];
    BEGIN ENABLE UNWIND =>
      BEGIN
      IF newFileHandle # NIL THEN CloseFile[fileSystem, newFileHandle, TRUE];
      CloseFile[fileSystem, currentFileHandle, FALSE];
      END;
    [newFileHandle, ] ← OpenFile[fileSystem, newFile, write, FALSE, @info];
  -- transfer contents of current file to new file
    ForkTransferPair[fileSystem, ReadFile, currentFileHandle, WriteFile, newFileHandle];
    temp ← newFileHandle;
    newFileHandle ← NIL;
    CloseFile[fileSystem, temp, FALSE];
    END; -- enable
    CloseFile[fileSystem, currentFileHandle, FALSE];
    DeleteFile[fileSystem, currentFile];
  END;

-- **********************!  Subroutines  !***********************

UniqueFileTag: ENTRY PROCEDURE RETURNS [tag: LONG CARDINAL] =
  BEGIN
  -- generate and return unique file tag
    tag ← uniqueFileTag ← uniqueFileTag+1;
  END;

-- **********************!  Main Program  !***********************

-- no operation    

END. -- of FTPAltoFile