-- FTPAltoFile.mesa,  Edit:
-- MAS Apr 21, 1980 5:37 PM  
-- HGM January 27, 1981  8:09 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 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],
  TimeExtra USING [PackedTimeFromString],
  DirExtraDefs USING [EnumerateDirectoryMasked];

FTPAltoFile: MONITOR
  -- Note:  UniqueFileTag constitutes the monitor.


  IMPORTS
    DiskKDDefs, Inline, Process, SegmentDefs, StreamDefs, String, Storage, Time,
    TimeExtra, 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 ← TimeExtra.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