-- file: FTPUserFiles.mesa, Edit: HGM July 31, 1980  5:33 PM  

-- Copyright  Xerox Corporation 1979, 1980

DIRECTORY
  FTPDefs,
  FTPPrivateDefs,
  Ascii USING [NUL],
  String USING [AppendChar, EquivalentString];

FTPUserFiles: PROGRAM
  IMPORTS String, FTPDefs, FTPPrivateDefs
  EXPORTS FTPDefs, FTPPrivateDefs
  SHARES FTPDefs =
BEGIN OPEN FTPDefs, FTPPrivateDefs;

 
FTPEnumerateFiles: PUBLIC PROCEDURE [ftpuser: FTPUser, remoteFiles: STRING, intent: Intent, processFile: PROCEDURE [UNSPECIFIED, STRING, VirtualFilename, FileInfo], processFileData: UNSPECIFIED] =
  BEGIN
  -- SendBlock procedure
    SendBlock: PROCEDURE [unused: UNSPECIFIED, source: POINTER,
        byteCount: CARDINAL] =
      BEGIN
      -- Note:  Required initial conditions are:
      --   Property list reset, property=FIRST[FileProperty], and value.length=0.
      -- check for premature end of file
        IF byteCount = 0 AND (property # FIRST[FileProperty] OR value.length # 0) THEN
          Abort[unexpectedEndOfFile];
      -- consume source data
        bytePointerObject ← [source, FALSE, byteCount];
        UNTIL bytePointerObject.count = 0 DO
          SELECT character ← LOOPHOLE[LoadByte[@bytePointerObject]] FROM
            spooledPropertyTerminator =>
              BEGIN
              WriteProperty[propertyList, property, value];
              value.length ← 0;
              IF property < LAST[FileProperty] THEN property ← SUCC[property]
              ELSE
                BEGIN
                -- read absolute and virtual filenames and file information
                  ReadFilename[file, propertyList, NIL, NIL];
                  ReadVirtualFilename[@virtualFilenameObject, propertyList];
                  ReadFileInfo[propertyList, @fileInfoObject];
                -- present filename to client for processing
                  WriteProperty[propertyList, serverFilename, file];
                  processFile[processFileData, file, @virtualFilenameObject, @fileInfoObject];
                -- advance to next property list
                  ResetPropertyList[propertyList];
                  property ← FIRST[FileProperty];
                END;
              END;
            ENDCASE => String.AppendChar[value, character];
          ENDLOOP; 
      END;
  -- ReceiveBlock procedure
    ReceiveBlock: PROCEDURE [unused: UNSPECIFIED, destination: POINTER,
        maxWordCount: CARDINAL] RETURNS [actualByteCount: CARDINAL] =
      BEGIN
      -- Note:  Required initial conditions are:
      --   property=LAST[FileProperty], value=NIL, and index=LAST[CARDINAL].
      -- produce destination data
        bytePointerObject ← [destination, FALSE, bytesPerWord*maxWordCount];
        UNTIL bytePointerObject.count = 0 OR endOfFile DO
          SELECT TRUE FROM
            (value # NIL AND index < value.length) =>
              BEGIN
              character ← value[index];
              index ← index + 1;
              END;
            (property < LAST[FileProperty]) =>
              BEGIN
              character ← spooledPropertyTerminator;
              property ← LOOPHOLE[LOOPHOLE[property, CARDINAL] + 1];
              value ← propertyList[property];
              index ← 0;
              END;
            ENDCASE =>
              BEGIN
              character ← IF index = LAST[CARDINAL]
                THEN Ascii.NUL ELSE spooledPropertyTerminator;
              [mark, code] ← GetCommand[ftper];
              SELECT mark FROM
                markHereIsPropertyList =>
                  BEGIN
                  GetPropertyList[ftper, propertyList];
                  property ← FIRST[FileProperty];
                  value ← propertyList[property];
                  index ← 0;
                  END;
                markNo =>
                  BEGIN
                  GetEOC[ftper];
                  AbortWithExplanation[CodeToSignal[code], ftper.inputString];
                  END;
                markEndOfCommand => endOfFile ← TRUE;
                ENDCASE => Abort[illegalProtocolSequence];
              END;
          IF character # Ascii.NUL THEN
            StoreByte[@bytePointerObject, LOOPHOLE[character]];
          ENDLOOP;
      -- compute actual byte count for caller
        actualByteCount ← bytesPerWord*maxWordCount - bytePointerObject.count;
      END;
  -- local constants
    filePrimitives: FilePrimitives = ftpuser.filePrimitives;
    ftper: FTPer = ftpuser.ftper;
    propertyList: PropertyList = ftpuser.propertyList;
    file: STRING = [maxStringLength];
    device: STRING = [maxStringLength];
    directory: STRING = [maxStringLength];
    name: STRING = [maxStringLength];
    version: STRING = [maxStringLength];
    absoluteValue: STRING = [maxStringLength];
  -- local variables
    enumerateState: {inactive, initiated} ← inactive;
    mark, code: Byte;
    fileHandle: FileHandle ← NIL;
    virtualFilenameObject: VirtualFilenameObject ←
      [device: device, directory: directory, name: name, version: version];
    fileInfoObject: FileInfoObject;
    bytePointerObject: BytePointerObject;
    property: FileProperty;
    value: STRING;
    index: CARDINAL;
    character: CHARACTER;
    endOfFile: BOOLEAN ← FALSE;
  -- verify purpose and state
    VerifyPurposeAndState[ftpuser, files, connected];
  -- send command
    mark ← SELECT intent FROM
      retrieval  => markRetrieve,
      deletion   => markDelete,
      ENDCASE => markDirectory; -- enumeration, renaming, unspecified
    PutCommand[ftper, mark, 0];
  -- construct property list containing absolute and virtual filenames and credentials
    ResetPropertyList[propertyList];
    WriteFilename[remoteFiles, propertyList, NIL, NIL, ftpuser.primaryPropertyList];
  -- send property list and EOC
    PutPropertyList[ftper, propertyList];  PutEOC[ftper];
  -- modify state to control reentry
    IF intent # unspecified THEN
      BEGIN
      ftpuser.state ← enumeratingFiles;
      ftpuser.intent ← intent;
      END;
    BEGIN ENABLE UNWIND =>
      BEGIN ENABLE FTPError => IF ftpError IN CommunicationError THEN CONTINUE;
      IF fileHandle # NIL THEN filePrimitives.CloseFile[ftpuser.fileSystem, fileHandle, TRUE];
      IF enumerateState = initiated THEN
        BEGIN
        IF intent#enumeration AND propertyList[serverFilename]#NIL THEN
          -- unwinding before starting to retrieve (or delete), reject offered file
          PutCommandAndEOC[ftper, markNo, codeDontSendFile];
        [] ← SkipRestOfFiles[ftpuser, intent IN [retrieval..deletion]];
        END;
      ftpuser.state ← connected;
      END;
  -- process files in-line
    IF intent IN [enumeration..deletion] THEN
      DO
        [mark, code] ← GetCommand[ftper];
        SELECT mark FROM
          markHereIsPropertyList =>
            BEGIN
            -- note enumerate initiated
              enumerateState ← initiated;
            -- receive property list and EOC
              GetPropertyList[ftper, propertyList];
              IF intent # enumeration THEN GetEOC[ftper];
            -- read absolute and virtual filenames and file information
              ReadFilename[file, propertyList, NIL, NIL];
              ReadVirtualFilename[@virtualFilenameObject, propertyList];
              ReadFileInfo[propertyList, @fileInfoObject];
            -- present filename to caller for processing
              WriteProperty[propertyList, serverFilename, file];
              processFile[processFileData, file, @virtualFilenameObject, @fileInfoObject];
            -- bypass file if not already retrieved/deleted
              IF intent # enumeration AND propertyList[serverFilename] # NIL THEN
                PutCommandAndEOC[ftper, markNo, codeDontSendFile];
            END;
          markNo =>
            BEGIN
            -- note enumerate inactive
              enumerateState ← inactive;
            -- receive EOC
              GetEOC[ftper];
            -- abort
              AbortWithExplanation[CodeToSignal[code], ftper.inputString];
            END;
          markEndOfCommand => EXIT;
          ENDCASE => Abort[illegalProtocolSequence];
        ENDLOOP
  -- process files out-of-line
    ELSE -- renaming, unspecified
      BEGIN
      -- Note:  file.length=0, requesting creation of scratch file.
      [fileHandle, ] ← filePrimitives.OpenFile[ftpuser.fileSystem, file, writeThenRead, FALSE, NIL];
      property ← LAST[FileProperty];  value ← NIL;  index ← LAST[CARDINAL];
      enumerateState ← initiated;  -- tell catch phrase what we are doing
      filePrimitives.WriteFile[ftpuser.fileSystem, fileHandle, ReceiveBlock, NIL];
      enumerateState ← inactive;
      ResetPropertyList[propertyList];  property ← FIRST[FileProperty];  value ← absoluteValue;
      filePrimitives.ReadFile[ftpuser.fileSystem, fileHandle, SendBlock, NIL];
      -- Note:  aborted=TRUE, requesting deletion of scratch file.
      filePrimitives.CloseFile[ftpuser.fileSystem, fileHandle, TRUE];
      END;
  -- reset state
    END; -- enable
    ftpuser.state ← connected;
  END;



FTPDeleteFile: PUBLIC PROCEDURE [ftpuser: FTPUser, remoteFile: STRING] =
  BEGIN OPEN ftpuser;
  -- local constants
    nextFile: STRING = propertyList[serverFilename];
  -- verify purpose and state
    VerifyPurposeAndState[ftpuser, files, 
      IF state = connected THEN connected ELSE enumeratingFiles];
  -- initiate and sustain remote delete
    IF state = connected THEN
      BEGIN
      -- send delete command
        PutCommand[ftper, markDelete, 0];
      -- construct property list containing absolute and virtual filenames and credentials
        ResetPropertyList[propertyList];
        WriteFilename[remoteFile, propertyList, NIL, NIL, primaryPropertyList];
      -- send property list and EOC
        PutPropertyList[ftper, propertyList];  PutEOC[ftper];
      -- sustain remote delete
        GetSpecificCommand[ftper, markHereIsPropertyList];
        GetPropertyList[ftper, propertyList];  GetEOC[ftper];
      END
  -- verify enumeration intent and filename
    ELSE
      BEGIN
      IF intent # deletion THEN Abort[illegalProcedureCallSequence];
      IF nextFile = NIL OR ~String.EquivalentString[remoteFile, nextFile] THEN
        Abort[filenameUnexpected];
      WriteProperty[propertyList, serverFilename, NIL];
      END;
  -- delete file
    PutCommandAndEOC[ftper, markYes, 0];
  -- terminate remote delete
    GetSpecificCommand[ftper, markYes];
    IF state = connected THEN FinishMultiFileOperation[ftpuser];
  END;

FTPRenameFile: PUBLIC PROCEDURE [ftpuser: FTPUser, currentFile, newFile: STRING] =
  BEGIN OPEN ftpuser;
  -- local constants
    nextFile: STRING = propertyList[serverFilename];
  -- verify purpose and state
    VerifyPurposeAndState[ftpuser, files, 
      IF state = connected THEN connected ELSE enumeratingFiles];
  -- verify enumeration intent and filename
    IF state = enumeratingFiles THEN
      BEGIN
      IF intent # renaming THEN Abort[illegalProcedureCallSequence];
      IF nextFile = NIL OR ~String.EquivalentString[currentFile, nextFile] THEN
        Abort[filenameUnexpected];
      WriteProperty[propertyList, serverFilename, NIL];
      END;
  -- send rename command
    PutCommand[ftper, markRename, 0];
  -- construct and send property list containing current absolute and virtual filenames and credentials
    ResetPropertyList[propertyList];
    WriteFilename[currentFile, propertyList, NIL, NIL, primaryPropertyList];
    PutPropertyList[ftper, propertyList];
  -- construct and send property list containing new absolute and virtual filenames
    ResetPropertyList[propertyList];
    WriteFilename[newFile, propertyList, NIL, NIL, secondaryPropertyList];
    PutPropertyList[ftper, propertyList];
  -- rename file
    PutEOC[ftper];
  -- terminate remote rename
    GetYesAndEOC[ftper];
  END;

-- **********************!  Filename Primitives  !***********************

FTPSetFilenameDefaults: PUBLIC PROCEDURE [ftpuser: FTPUser, status: Status, virtualFilename: VirtualFilename] =
  BEGIN OPEN ftpuser;
  -- local constants
    propertyList: PropertyList = IF status = primary
      THEN primaryPropertyList
      ELSE  secondaryPropertyList;
  -- record virtual filename in appropriate property list
    WriteVirtualFilename[virtualFilename, propertyList, FALSE];
  END;

FTPNoteFilenameUsed: PUBLIC PROCEDURE [ftpuser: FTPUser, absoluteFilename: STRING, virtualFilename: VirtualFilename] =
  BEGIN OPEN ftpuser;
  -- return absolute filename
    IF absoluteFilename # NIL THEN ReadFilename[absoluteFilename, propertyList, NIL, NIL];
  -- return virtual filename
    IF virtualFilename # NIL THEN ReadVirtualFilename[virtualFilename, propertyList];
  END;


-- **********************!  Protocol Subroutine  !***********************

FinishMultiFileOperation: PUBLIC PROCEDURE [ftpuser: FTPUser] =
  BEGIN
  IF SkipRestOfFiles[ftpuser,TRUE] THEN Abort[fileGroupDesignatorUnexpected];
  END;

SkipRestOfFiles: PROCEDURE [ftpuser: FTPUser, sayNo: BOOLEAN]
  RETURNS [fileBypassed: BOOLEAN] =
  BEGIN OPEN ftpuser;
  mark, code: Byte;
  fileBypassed ← FALSE;
  DO
    [mark, code] ← GetCommand[ftper];
    SELECT mark FROM
      markHereIsPropertyList =>
        BEGIN
        GetPropertyList[ftper, propertyList];
        IF sayNo THEN
          BEGIN
          GetEOC[ftper];
          PutCommandAndEOC[ftper, markNo, codeDontSendFile];
          fileBypassed ← TRUE;
          END;
        END;
      markNo =>
        BEGIN
        GetEOC[ftper];
        AbortWithExplanation[CodeToSignal[code], ftper.inputString];
        END;
      markEndOfCommand => EXIT;
      ENDCASE => Abort[illegalProtocolSequence];
    ENDLOOP;
  END;

StoreByte: PUBLIC PROCEDURE [dstBytePointer: BytePointer, byte: Byte] = INLINE
  BEGIN
  -- Note:  Doesn't check for byte pointer exhaustion.
  -- local constants
    dBP: BytePointer = dstBytePointer;
    dWord: Word = dBP.address;
  -- store byte
    IF dBP.offset THEN dWord.rhByte ← byte ELSE dWord.lhByte ← byte;
  -- advance address and offset
    IF ~(dBP.offset ← ~dBP.offset) THEN dBP.address ← dBP.address + 1;
  -- decrement byte count
    dBP.count ← dBP.count - 1;
  END;

LoadByte: PUBLIC PROCEDURE [srcBytePointer: BytePointer] RETURNS [byte: Byte] = INLINE
  BEGIN
  -- Note:  Doesn't check for byte pointer exhaustion.
  -- local constants
    sBP: BytePointer = srcBytePointer;
    sWord: Word = sBP.address;
  -- load byte
    byte ← IF sBP.offset THEN sWord.rhByte ELSE sWord.lhByte;
  -- advance address and offset
    IF ~(sBP.offset ← ~sBP.offset) THEN sBP.address ← sBP.address + 1;
  -- decrement byte count
    sBP.count ← sBP.count - 1;
  END;


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

-- no operation

END. -- of FTPUserFiles