-- FTPProtocol.mesa, Edit: HGM July 31, 1980  11:05 PM
-- HGM's version of GetPropertyList spliced in by MDS on
--           June 28, 1982  4:06 PM  
-- Edited by Andrew Birrell on September 10, 1982 1:32 pm ("~IN" -> "NOT IN")

-- Copyright  Xerox Corporation 1979, 1980

DIRECTORY
  FTPDefs,
  FTPPrivateDefs,
  String USING [AppendChar, AppendString, EquivalentString, StringBoundsFault],
  Storage USING [Node, String, Free, FreeStringNil];

FTPProtocol: PROGRAM
  -- import list
    IMPORTS String, Storage, FTPPrivateDefs
  -- export list
    EXPORTS FTPPrivateDefs
  -- share list
    SHARES FTPDefs =
BEGIN OPEN FTPDefs, FTPPrivateDefs;
 
-- **********************!  Constants  !***********************

ftpsystem: POINTER TO FTPSystem = LocateFtpSystemObject[];

-- **********************!  Command/Response Shorthands  !***********************

PutGetVersion: PUBLIC PROCEDURE [ftper: FTPer, version: Byte] =
  BEGIN
  -- Note:  Obtains local command string from ftper.outputString;
  --   returns remote command string in ftper.inputString.
  -- local variables
    mark, code: Byte;
  -- send IAmVersion command and EOC
    PutCommandAndEOC[ftper, markIAmVersion, version];
  -- receive echoing IAmVersion command and EOC in response
    [mark, code] ← GetCommandAndEOC[ftper];
    IF mark # markIAmVersion THEN Abort[illegalProtocolSequence];
  -- verify protocol version number
    IF code # version THEN Abort[protocolVersionMismatch];
  END;

GetAnswerAndEOC: PUBLIC PROCEDURE [ftper: FTPer] RETURNS [answer: BOOLEAN] =
  BEGIN
  -- local variables
    mark, code: Byte;
  -- receive command and EOC
    [mark, code] ← GetCommandAndEOC[ftper];
  -- decode answer
    SELECT mark FROM
      markYes => answer ← TRUE;
      markNo   => answer ← FALSE;
      ENDCASE => Abort[illegalProtocolSequence];
  END;

GetYesAndEOC: PUBLIC PROCEDURE [ftper: FTPer] =
  BEGIN
  -- receive Yes command
    GetSpecificCommand[ftper, markYes];
  -- receive EOC
    GetEOC[ftper];
  END;

GetSpecificCommand: PUBLIC PROCEDURE [ftper: FTPer, specificMark: Byte] =
  BEGIN
  -- Note:  If a No command is encountered, flushes the EOC that follows
  --   and uses No's parameters as the basis for an abort.
  -- local variables
    mark, code: Byte;
  -- receive command
    [mark, code] ← GetCommand[ftper];
  -- verify that expected command received
    SELECT mark FROM
      specificMark => NULL;
      markNo =>
        BEGIN
        -- receive EOC
          GetEOC[ftper];
        -- abort
          AbortWithExplanation[CodeToSignal[code], ftper.inputString];
        END;
      ENDCASE => Abort[illegalProtocolSequence];
  END;

PutCommandAndEOC: PUBLIC PROCEDURE [ftper: FTPer, mark, code: Byte] =
  BEGIN
  -- send command
    PutCommand[ftper, mark, code];
  -- send EOC
    PutEOC[ftper];
  END;

GetCommandAndEOC: PUBLIC PROCEDURE [ftper: FTPer] RETURNS [mark, code: Byte] =
  BEGIN
  -- receive command
    [mark, code] ← GetCommand[ftper];
  -- receive EOC
    GetEOC[ftper];
  END;

PutEOC: PUBLIC PROCEDURE [ftper: FTPer] =
  BEGIN OPEN ftper;
  -- Note:  Forces transmission.
  -- send EOC
    PutCommand[ftper, markEndOfCommand, 0];
  -- force its transmission
    communicationPrimitives.ForceOutput[communicationSystem, connection];
  END;

GetEOC: PUBLIC PROCEDURE [ftper: FTPer] =
  BEGIN
  -- receive EOC
    GetSpecificCommand[ftper, markEndOfCommand];
  END;

-- **********************!  Command/Response Primitives  !***********************

PutCommand: PUBLIC PROCEDURE [ftper: FTPer, mark, code: Byte] =
  BEGIN OPEN ftper;
  -- Note:  Obtains command string, whenever appropriate, from ftper.outputString.
  -- local variables
    codeAppropriate, stringAppropriate: BOOLEAN;
  -- trace change of direction (if any) and mark
    IF tracing THEN TraceOut[ftper, mark];
  -- send and classify mark
    communicationPrimitives.ProduceDiscontinuity[communicationSystem, connection];
    communicationPrimitives.SendByte[communicationSystem, connection, mark];
    [codeAppropriate, stringAppropriate] ← ClassifyCommand[mark];
  -- send code if any (which is traced by PutCharacter)
    IF codeAppropriate THEN PutCharacter[ftper, code];
  -- send string if any (which is traced by PutString)
    IF stringAppropriate AND outputString.length # 0 THEN
      BEGIN
      PutString[ftper, outputString];
      outputString.length ← 0;
      END;
  END;

GetCommand: PUBLIC PROCEDURE [ftper: FTPer] RETURNS [mark, code: Byte] =
  BEGIN OPEN ftper;
  -- Note:  Flushes comments; returns zero if no code;
  --    returns command string, whenever appropriate, in ftper.inputString.
  -- local variables
    settledForNone: BOOLEAN;
    codeAppropriate, stringAppropriate: BOOLEAN;
  -- trace change of direction (if any)
    IF tracing THEN TraceIn[ftper];
  -- initialize code to none
    code ← 0;
  -- receive commands until non-comment encountered
    mark ← markComment;
    WHILE mark = markComment DO
      -- flush until next mark
          [, settledForNone] ←
            communicationPrimitives.ReceiveByte[communicationSystem, connection, TRUE];
          IF ~settledForNone THEN SkipThingsFaster[ftper];
        communicationPrimitives.ConsumeDiscontinuity[communicationSystem, connection];
      -- receive and classify mark
        mark ← GetCharacter[ftper, FALSE];
        [codeAppropriate, stringAppropriate] ← ClassifyCommand[mark];
      -- receive code if any
        IF codeAppropriate THEN code ← GetCharacter[ftper, FALSE];
      -- receive string if any
        IF stringAppropriate THEN GetString[ftper, inputString, 0];
      ENDLOOP;
  END;

-- Used to skip to the end of a file if we abort during a transfer
SkipThingsFaster: PROCEDURE [ftper: FTPer] =
  BEGIN OPEN ftper;
  trash: ARRAY [0..100) OF WORD;
  bpo: BytePointerObject;
  DO
    bpo ← [@trash,FALSE,2*100];
    communicationPrimitives.ReceiveBytes[
      communicationSystem, connection, @bpo, FALSE];
    IF bpo.count#0 THEN EXIT;
    ENDLOOP;
  END;

ClassifyCommand: PROCEDURE [mark: Byte] RETURNS [codeAppropriate, stringAppropriate: BOOLEAN] =
  BEGIN
  -- initialize classification
    codeAppropriate ← stringAppropriate ← FALSE;
  -- interpret mark
    SELECT mark FROM
      NOT IN [markMinimum..markMaximum] => Abort[functionNotImplemented];
      markYes, markNo, markIAmVersion, markMailboxException =>
        codeAppropriate ← stringAppropriate ← TRUE;
      markComment, markYouAreUser, markAbort => stringAppropriate ← TRUE;
      ENDCASE;
  END;

PutPropertyList: PUBLIC PROCEDURE [ftper: FTPer, propertyList: PropertyList] =
  BEGIN
  -- PutProperty procedure
    PutProperty: PROCEDURE [property: Property, value: STRING] =
      BEGIN
      -- local constants
        encodedValue: STRING = [maxStringLength];
      -- local variables
        i: CARDINAL;
      -- send left delimiter
        PutCharacter[ftper, propertyLeftDelimiter];
      -- send property name
        PutString[ftper, ftpsystem.propertyNames[property]];
      -- send separator
        PutCharacter[ftper, propertyNameValueSeparator];
      -- precede reserved characters with escape
        FOR i IN [0..value.length) DO
          SELECT value[i] FROM
            propertyLeftDelimiter, propertyRightDelimiter, propertyEscape =>
              String.AppendChar[encodedValue, propertyEscape];
            ENDCASE;
          String.AppendChar[encodedValue, value[i]];
          ENDLOOP;
      -- send property value
        PutString[ftper, encodedValue];
      -- send right delimiter
        PutCharacter[ftper, propertyRightDelimiter];
      END;
  -- local variables
    property: Property;
  -- send left delimiter
    PutCharacter[ftper, propertyListLeftDelimiter];
  -- send properties
    FOR property IN Property DO
      IF propertyList[property] # NIL THEN
        PutProperty[property, propertyList[property]];
      ENDLOOP;
  -- send right delimiter
    PutCharacter[ftper, propertyListRightDelimiter];
  END;

GetPropertyList: PUBLIC PROCEDURE [ftper: FTPer, propertyList: PropertyList] =
    BEGIN
    -- Note:  Caller may handle optional property list case
    --   by intercepting protocolParameterListMissing.
    ResetPropertyList[propertyList];
    IF GetCharacter[ftper, TRUE] # propertyListLeftDelimiter THEN
      Abort[protocolParameterListMissing];
    DO
      SELECT GetCharacter[ftper, FALSE] FROM
	propertyLeftDelimiter =>
	  BEGIN
          name: STRING = [maxStringLength];
          value: STRING = [maxStringLength];
          BEGIN OPEN ftper;
          character: CHARACTER;
          GetString[ftper, name, propertyNameValueSeparator];
          value.length ← 0;
          UNTIL (character ← GetCharacter[ftper, FALSE]) = propertyRightDelimiter DO
	    IF character = propertyEscape THEN character ← GetCharacter[ftper, FALSE];
	    String.AppendChar[value, character ! String.StringBoundsFault => Abort[stringTooLong]];
	    ENDLOOP;
          END;
          FOR property: Property IN Property DO
            IF String.EquivalentString[name, ftpsystem.propertyNames[property]] THEN
	      BEGIN
	      WriteProperty[propertyList, property, value];
	      EXIT;
	      END;
	    ENDLOOP;  -- Ignore unrecognized properties
	  END;
	propertyListRightDelimiter => EXIT;
	ENDCASE => Abort[illegalProtocolParameterList];
      ENDLOOP;
    END;

PutString: PUBLIC PROCEDURE [ftper: FTPer, string: STRING] =
  BEGIN OPEN ftper;
  -- local variables
    bytePointerObject: BytePointerObject ← [@string.text, FALSE, string.length];
  -- trace string
    IF tracing THEN TraceString[ftper, string];
  -- send string
    communicationPrimitives.SendBytes[communicationSystem, connection, @bytePointerObject];
  END;

GetString: PUBLIC PROCEDURE [ftper: FTPer, string: STRING, terminator: UNSPECIFIED] =
  BEGIN OPEN ftper;
  -- Note:  Declaring terminator UNSPECIFIED admits both CHARACTER and Byte;
  --   null teminator implies mark.
  -- local constants
    rathole: STRING = [maxStringLength];
  -- local variables
    byte: Byte;
    character: CHARACTER;
    bytePointerObject: BytePointerObject;
  -- initialize string
    IF string = NIL THEN string ← rathole
    ELSE string.length ← 0;
  -- receive characters until terminating character encountered
    IF terminator # 0 THEN
      BEGIN
      -- receive string and terminator
        DO
          [byte, ] ←
            communicationPrimitives.ReceiveByte[communicationSystem, connection, FALSE];
          IF (character ← LOOPHOLE[byte]) = terminator THEN EXIT;
          String.AppendChar[string, character !
            String.StringBoundsFault => Abort[stringTooLong]];
          ENDLOOP;
      -- trace string and terminator
        IF tracing THEN
          BEGIN
          TraceString[ftper, string];
          TraceCharacter[ftper, character];
          END;
      END
  -- receive characters until mark encountered
    ELSE
      BEGIN
      -- receive string
        bytePointerObject ← [@string.text, FALSE, string.maxlength];
        communicationPrimitives.ReceiveBytes[
          communicationSystem, connection, @bytePointerObject, TRUE];
        string.length ← string.maxlength - bytePointerObject.count;
      -- trace string
        IF tracing THEN TraceString[ftper, string];
      -- abort if string too long
        IF string.length = string.maxlength AND GetCharacter[ftper, TRUE] # 0 THEN
          Abort[stringTooLong];
      END;
  END;

PutCharacter: PROCEDURE [ftper: FTPer, character: UNSPECIFIED] =
  BEGIN OPEN ftper;
  -- Note:  Declaring character UNSPECIFIED admits both CHARACTER and Byte.
  -- trace character
    IF tracing THEN TraceCharacter[ftper, character];
  -- send character
    communicationPrimitives.SendByte[communicationSystem, connection, character];
  END;

GetCharacter: PROCEDURE [ftper: FTPer, settleForNone: BOOLEAN] RETURNS [character: UNSPECIFIED] =
  BEGIN OPEN ftper;
  -- Note:  Declaring character UNSPECIFIED admits both CHARACTER and Byte;
  --   returns null character if mark pending.
  -- local variables
    settledForNone: BOOLEAN;
  -- receive character or discover mark pending
    [character, settledForNone] ←
      communicationPrimitives.ReceiveByte[communicationSystem, connection, settleForNone];
  -- trace character or supply null character 
    IF settledForNone THEN character ← 0
    ELSE IF tracing THEN TraceCharacter[ftper, character];
  END;


-- **********************!  Property List Primitives  !***********************

CreatePropertyList: PUBLIC PROCEDURE RETURNS [propertyList: PropertyList] =
  BEGIN
  -- local variables
    property: Property;
    pointer: POINTER TO PropertyListObject;
  -- allocate property list object
    pointer ← Storage.Node[SIZE[PropertyListObject]];
    propertyList ← DESCRIPTOR[pointer↑];
  -- initialize property list to empty
    FOR property IN Property DO
      propertyList[property] ← NIL;
      ENDLOOP;
  END;

DestroyPropertyList: PUBLIC PROCEDURE [propertyList: PropertyList] =
  BEGIN
  -- release property list values
    ResetPropertyList[propertyList];
  -- release property list object
    Storage.Free[BASE[propertyList]];
  END;

CopyPropertyList: PUBLIC PROCEDURE [srcPropertyList, dstPropertyList: PropertyList] =
  BEGIN
  -- local variables
    property: Property;
  -- copy property values
    FOR property IN Property DO
      WriteProperty[dstPropertyList, property, srcPropertyList[property]];
      ENDLOOP;
  END;

ResetPropertyList: PUBLIC PROCEDURE [propertyList: PropertyList] =
  BEGIN
  -- local variables
    property: Property;
  -- release property values and reset to NIL
    FOR property IN Property DO
      propertyList[property] ← Storage.FreeStringNil[propertyList[property]];
      ENDLOOP;
  END;

WriteProperty: PUBLIC PROCEDURE [propertyList: PropertyList, property: Property, value: STRING] =
  BEGIN
  -- set property value to NIL
    IF value = NIL OR value.length = 0 THEN
      -- release current property value
      propertyList[property] ← Storage.FreeStringNil[propertyList[property]]
  -- set property value to string
    ELSE
      BEGIN
      -- allocate string to hold new property value
        IF propertyList[property] = NIL THEN
          propertyList[property] ← Storage.String[value.length]
      -- use current string if possible
        ELSE
          IF value.length > propertyList[property].maxlength THEN
            BEGIN
            propertyList[property] ← Storage.FreeStringNil[propertyList[property]];
            propertyList[property] ← Storage.String[value.length];
            END
          ELSE propertyList[property].length ← 0;
      -- record new property value
        String.AppendString[propertyList[property], value];
      END;
  END;

EncodeBooleanProperty: PUBLIC PROCEDURE [boolean: BOOLEAN, property: STRING] =
  BEGIN
  -- Note:  Defaults to FALSE.
  -- encode boolean property
    property.length ← 0;
    IF boolean THEN String.AppendString[property, "Yes"L];
  END;

DecodeBooleanProperty: PUBLIC PROCEDURE [property: STRING] RETURNS [boolean: BOOLEAN] =
  BEGIN
  -- Note:  Defaults to FALSE.
  -- decode boolean property
    SELECT TRUE FROM
      (property = NIL),
      String.EquivalentString[property, "No"L] => boolean ← FALSE;
      String.EquivalentString[property, "Yes"L] => boolean ← TRUE;
      ENDCASE => Abort[illegalBooleanParameter];
  END;


END. -- of FTPProtocol