-- 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