-- Copyright (C) 1979, 1980, 1984, 1985  by Xerox Corporation. All rights reserved. 
-- FTPUserFiles.mesa, HGM, 15-Sep-85 16:11:06  

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: LONG UNSPECIFIED, source: LONG 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: LONG UNSPECIFIED, destination: LONG 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
      foo: LONG POINTER ← NIL;
      -- 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, foo];
      enumerateState ← inactive;
      ResetPropertyList[propertyList];
      property ← FIRST[FileProperty];
      value ← absoluteValue;
      filePrimitives.ReadFile[ftpuser.fileSystem, fileHandle, SendBlock, foo];
      -- 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