DIRECTORY BasicTime USING [GMT, MonthOfYear, Unpack, Unpacked], Convert USING [IntFromRope], IO USING [Close, CreateStream, CreateStreamProcs, EndOf, EndOfStream, Error, GetBlock, GetChar, GetIndex, GetLength, int, PutBlock, PutChar, PutF, PutFR, PutRope, RopeFromROS, ROS, SetIndex, STREAM, StreamProcs, UnsafeBlock, UnsafeGetBlock, UnsafePutBlock], Pup USING [Address], PupName USING [NameLookup, Error], PupStream USING [Abort, CloseReason, ConsumeMark, Create, SendMark, StreamClosing, Timeout, waitForever], PupWKS USING [ftp], RefText USING [Append, AppendChar, AppendRope, Equal, Length, ObtainScratch, ReleaseScratch, TrustTextAsRope], Rope USING [Equal, Fetch, Find, FromRefText, Length, ROPE], STP USING [Open, DesiredProperties, Access, Completion, CompletionProcType, Confirmation, ConfirmProcType, CredentialsErrors, ErrorCode, FileErrors, FileInfo, FileInfoObject, NoteFileProcType, Type, ValidProperties], STPBackdoor, STPPrivate; STPImpl: CEDAR PROGRAM IMPORTS BasicTime, Convert, IO, PupName, PupStream, RefText, Rope, STP EXPORTS STP, STPBackdoor = { OPEN STPPrivate; ErrorCode: TYPE = STP.ErrorCode; ROPE: TYPE = Rope.ROPE; Object: PUBLIC TYPE ~ STPPrivate.Object; -- export to STP Error: PUBLIC SIGNAL [stp: Handle, code: ErrorCode, error: ROPE, reply: CHAR _ 0C] = CODE; MarkEncountered: ERROR = CODE; BadProperty: PUBLIC ERROR = CODE; propertyStrings: PListArray = [ userName: "User-Name", userPassword: "User-Password", connectName: "Connect-Name", connectPassword: "Connect-Password", byteSize: "Byte-Size", type: "Type", size: "Size", directory: "Directory", nameBody: "Name-Body", version: "Version", createDate: "Creation-Date", readDate: "Read-Date", writeDate: "Write-Date", author: "Author", eolConversion: "End-of-Line-Convention", account: "Account", userAccount: "User-Account", device: "Device", serverName: "Server-Filename"]; desiredPropString: REF TEXT = "Desired-property"; Close: PUBLIC PROC [stp: Handle] = { nilByteStream: BOOLEAN; IF stp = NIL THEN RETURN; nilByteStream _ stp.byteStream = NIL; CloseInternal[stp]; IF nilByteStream THEN ERROR Error[stp: stp, code: noConnection, error: "Attempt to Close a NIL connection"]; }; CloseInternal: PROC [stp: Handle] = { ResetPList[stp.plist]; IF stp.byteStream # NIL THEN { IO.Close[stp.byteStream]; stp.byteStream _ NIL; }; }; Create: PUBLIC PROC RETURNS [stp: Handle] = { stp _ NEW[Object _ []]; stp.info _ NEW[STP.FileInfoObject _ []]; stp.remoteString _ NEW[TEXT[100]]; stp.plist _ NEW[PListArray]; FOR p: ValidProperties IN ValidProperties DO stp.plist[p] _ NEW[TEXT[100]]; ENDLOOP; FOR u: UserProperties IN UserProperties DO stp.userState[u] _ NEW[TEXT[100]]; ENDLOOP; RETURN[stp]; }; Destroy: PUBLIC PROC [stp: Handle] RETURNS [Handle] = { IF stp = NIL THEN RETURN[NIL]; IF stp.byteStream # NIL THEN Close[stp]; RETURN[NIL]; }; IsOpen: PUBLIC PROC [stp: Handle] RETURNS [yes: BOOL] = { IF stp.byteStream = NIL THEN RETURN[FALSE]; yes _ TRUE; [] _ IO.EndOf[stp.byteStream ! PupStream.StreamClosing => { yes _ FALSE; CONTINUE; } ]; }; Open: PUBLIC PROC [stp: Handle, host: ROPE] RETURNS [herald: ROPE] = { reason: PupStream.CloseReason; IF stp = NIL THEN RETURN["\"NIL\" STP handle"]; IF IsOpen[stp] THEN ERROR Error[stp, alreadyAConnection, "You already have a connection?"]; IF stp.byteStream # NIL THEN CloseInternal[stp]; { ENABLE PupStream.StreamClosing => {reason _ why; GOTO StreamClosing}; server: Pup.Address = PupName.NameLookup[host, PupWKS.ftp ! PupName.Error => GenerateErrorString[stp, noSuchHost, text]]; stp.host _ NIL; stp.byteStream _ PupStream.Create[server, PupStream.waitForever, PupStream.waitForever]; stp.gotMark _ FALSE; PutCommand[stp, iAmVersion, 1C, "STP calling"]; { code: CHAR; mark: Mark; [mark, code] _ GetCommand[stp]; IF mark # iAmVersion THEN GenerateProtocolError[stp, badVersion, mark, code]; ErrorIfNextNotEOC[stp]; herald _ Rope.FromRefText[stp.remoteString]; }; stp.host _ host; EXITS StreamClosing => {CloseInternal[stp]; GenerateStreamClosingError[stp, reason]}; }; }; ErrorCodeToSTPErrorCode: PROC [errorCode: ErrorCode, code: CHAR] RETURNS [ErrorCode] = { replyCode: Reply = LOOPHOLE[code]; RETURN[SELECT replyCode FROM null => errorCode, badCommand => protocolError, noUserName => illegalUserName, illegalCommand => protocolError, badPList => protocolError, illegalServerFilename => illegalFileName, illegalDirectory => illegalFileName, illegalNameBody => illegalFileName, illegalVersion => illegalFileName, illegalType => accessError, illegalCharacterSize => accessError, illegalEOLConversion => accessError, illegalUserName => illegalUserName, illegalUserPassword => illegalUserPassword, illegalUserAccount => illegalUserAccount, illegalConnectName => illegalConnectName, illegalConnectPassword => illegalConnectPassword, illegalCreationDate => illegalFileName, illegalWriteDate => illegalFileName, illegalReadDate => illegalFileName, illegalAuthor => illegalFileName, illegalDevice => illegalFileName, fileNotFound => noSuchFile, accessDenied => accessDenied, inconsistent => protocolError, fileDataError => errorCode, tooLong => requestRefused, dontSend => errorCode, notCompleted => errorCode, transientError => errorCode, permanentError => errorCode, fileBusy => accessError, ENDCASE => errorCode] -- can't do any better--}; GenerateError: PROC [stp: Handle, errorCode: ErrorCode, code: CHAR _ 0C] = { string: ROPE _ NIL; IF RefText.Length[stp.remoteString] # 0 THEN string _ Rope.FromRefText[stp.remoteString]; GenerateErrorString[stp, errorCode, string, code]; }; GenerateErrorString: PROC [stp: Handle, errorCode: ErrorCode, string: ROPE, code: CHAR _ 0C] = { ERROR Error[stp, ErrorCodeToSTPErrorCode[errorCode, code], IF Rope.Length[string] # 0 THEN string ELSE SELECT errorCode FROM noSuchHost => "No such host!", noRouteToNetwork => "No route to network!", noNameLookupResponse => "Name lookup server is not responding", alreadyAConnection => "You already have a connection!", noConnection => "Please open a connection!", connectionClosed => "Connection closed (local or remote)!", connectionRejected => "Connection rejected by remote host!", connectionTimedOut => "Connection timed out!", accessDenied => "Access denied by remote server!", illegalUserName => "Invalid or illegal UserName!", illegalUserPassword => "Invalid or illegal UserPassword!", illegalUserAccount => "Invalid or illegal UserAccount!", illegalConnectName => "Invalid or illegal ConnectName!", illegalConnectPassword => "Invalid or illegal ConnectPassword!", credentailsMissing => "Name and/or Password not supplied!", protocolError => "Internal FTP protocol error!", illegalFileName => "Illegal filename!", noSuchFile => "File not found!", requestRefused => "Request refused by remote host!", accessError => "Illegal access attempt on remote stream!", undefinedError => "Undefined error!", ENDCASE => ERROR, code]; }; GenerateStreamClosingError: PROC [stp: Handle, why: PupStream.CloseReason] = { GenerateErrorString[stp, SELECT why FROM localClose, remoteClose => connectionClosed, noRouteToNetwork => noRouteToNetwork, transmissionTimeout => connectionTimedOut, remoteReject => connectionRejected, ENDCASE => ERROR, NIL]; }; GenerateProtocolError: PROC [ stp: Handle, type: ProtocolError, mark: Mark, code: CHAR _ 0C] = { text: ROPE = IO.PutFR["%g, mark = %b, code = %b", [rope[SELECT type FROM badVersion => "Incompatable protocol version", badMark => "Invalid or undefined mark byte", badPList => "Invalid or malformed property list", eocExpected => "End-Of-Command mark byte expected", noCode => "error code is required after error mark byte", ENDCASE => ERROR]], [integer[mark.ORD]], [integer[code.ORD]] ]; ERROR Error[stp, protocolError, text, code]; }; SelectError: PROC [stp: Handle, s: ROPE, mark: Mark] = { code: CHAR _ 0C; IF mark = no OR mark = comment THEN { IF mark # comment THEN code _ CollectCode[stp]; CollectString[stp]; GenerateError[stp, requestRefused, code]; } ELSE GenerateProtocolError[stp, badMark, mark, code]; }; Delete: PUBLIC PROC [stp: Handle, name: Rope.ROPE, confirm: STP.ConfirmProcType, complete: STP.CompletionProcType] = { DoFiles[stp, name, confirm, complete, delete]; }; Enumerate: PUBLIC PROC [stp: Handle, name: Rope.ROPE, proc: STP.NoteFileProcType] = { Foo1: STP.ConfirmProcType = { RETURN[answer: IF proc[file] = yes THEN do ELSE abort, localStream: NIL]; }; Foo2: STP.CompletionProcType = { }; DoFiles[stp, name, Foo1, Foo2, directory]; }; Rename: PUBLIC PROC [stp: Handle, old, new: Rope.ROPE] = { mark, saveMark: Mark; code: CHAR _ 0C; CheckConnection[stp]; ResetPList[stp.plist]; UserStateToPList[stp]; NameToPList[stp.plist, old]; PutPList[stp, rename, FALSE]; ResetPList[stp.plist]; UserStateToPList[stp]; NameToPList[stp.plist, new]; PutPList[stp, null]; [saveMark, code] _ GetCommand[stp]; IF (mark _ MyGetMark[stp]) # eoc THEN GenerateProtocolError[stp, eocExpected, mark]; IF saveMark # yes THEN GenerateError[stp, requestRefused, code]; }; Retrieve: PUBLIC PROC [stp: Handle, file: Rope.ROPE, confirm: STP.ConfirmProcType _ NIL, complete: STP.CompletionProcType _ NIL] = { DoFiles[stp, file, confirm, complete, retrieve]; }; Store: PUBLIC PROC [stp: Handle, file: Rope.ROPE, stream: IO.STREAM, noteFile: STP.NoteFileProcType _ NIL, fileType: STP.Type, creation: BasicTime.GMT] = { size: INT _ 0; Confirm: STP.ConfirmProcType = { IF noteFile = NIL OR noteFile[file] = yes THEN RETURN[do, stream] ELSE RETURN[skip, NIL]}; CheckConnection[stp]; ResetPList[stp.plist]; UserStateToPList[stp]; NameToPList[stp.plist, file]; IF fileType = unknown THEN fileType _ FindFileType[stream]; SetFileType[stp, fileType]; SetByteSize[stp, fileType]; SetCreateTime[stp, creation]; size _ stream.GetLength[ ! IO.Error => CONTINUE;]; IF size # 0 THEN SetPListItem[stp.plist, "Size", IO.PutFR["%g", IO.int[size]]]; DoFiles[stp, file, Confirm, NIL, store]; }; DoFiles: PUBLIC PROC [stp: Handle, file: Rope.ROPE, confirm: STP.ConfirmProcType, complete: STP.CompletionProcType, op: STPPrivate.Operation] = { reason: PupStream.CloseReason; reply: STP.Confirmation; string: Rope.ROPE _ NIL; local: IO.STREAM _ NIL; killConnection: BOOLEAN _ TRUE; CleanUp: PROC = { IF killConnection THEN SmashClosed[stp]; }; {ENABLE { PupStream.StreamClosing => { reason _ why; GOTO StreamClosing; }; PupStream.Timeout => { reason _ transmissionTimeout; GOTO StreamClosing; }; UNWIND => CleanUp[]}; IF op # store THEN { CheckConnection[stp]; ResetPList[stp.plist]; UserStateToPList[stp]; NameToPList[stp.plist, file]; }; IF op = directory THEN SELECT TryNewDirectory[stp, confirm ! Error => IF code IN STP.CredentialsErrors OR code IN STP.FileErrors THEN killConnection _ FALSE] FROM finished => RETURN; aborted => { CleanUp[]; RETURN; }; tryOldDirectory => NULL; -- just falls thru ENDCASE => ERROR; PutPList[ stp, SELECT op FROM delete => delete, directory => directory, retrieve => retrieve, store => newStore, ENDCASE => ERROR]; DO IF LookAtMark[stp] = eoc THEN { [] _ MyGetMark[stp]; EXIT; }; GetHereIsAndPList[stp, op # directory ! Error => IF code IN STP.CredentialsErrors OR code IN STP.FileErrors THEN killConnection _ FALSE]; SELECT TRUE FROM confirm = NIL => {reply _ do; local _ NIL}; ENDCASE => [reply, local] _ confirm[string _ MakeRemoteName[stp.plist]]; SELECT reply FROM do => { completion: STP.Completion; SELECT op FROM delete => { code: CHAR _ 0C; mark: Mark; PutCommand[stp, yes, 0C, "Yes, please"]; [mark, code] _ GetCommand[stp]; SELECT mark FROM yes => completion _ ok; no => IF complete = NIL THEN GenerateError[stp, requestRefused, code] ELSE completion _ error; ENDCASE => GenerateError[stp, protocolError, code]}; retrieve => { code: CHAR _ 0C; gotIt: BOOLEAN; PutCommand[stp, yes, 0C, "Yes, please"]; [gotIt, code] _ GetFile[stp, local]; completion _ IF gotIt THEN ok ELSE error; IF completion = error AND complete = NIL THEN GenerateError[stp, requestRefused, code]; }; store => { PutFile[stp, local]; ErrorIfNextNotYes[stp]}; ENDCASE; IF complete # NIL THEN complete[completion, Rope.FromRefText[stp.remoteString]]; IF LookAtMark[stp] = eoc THEN { [] _ MyGetMark[stp]; EXIT; }; }; skip => { IF op # directory THEN PutCommand[stp, no, 106C, "No Thanks"]; IF op = store THEN { mark: Mark; code: CHAR; [mark, code] _ GetCommand[stp]; IF mark # no THEN GenerateError[stp, protocolError, code]}; }; abort => { CleanUp[]; RETURN; }; ENDCASE => ERROR; ResetPList[stp.plist]; ENDLOOP; EXITS StreamClosing => GenerateStreamClosingError[stp, reason]}; }; TryNewDirectory: PROC[stp: Handle, confirm: STP.ConfirmProcType] RETURNS [{finished, aborted, tryOldDirectory}] = { mark: Mark; PutPList[stp, newDirectory]; DO SELECT (mark _ MyGetMark[stp]) FROM comment => CollectString[stp]; hereIsPList => { string: Rope.ROPE; reply: STP.Confirmation; DO GetPList[stp: stp, gobbleEOC: FALSE, propertiesOk: TRUE ! MarkEncountered => { mark _ LookAtMark[stp]; IF mark = eoc THEN EXIT ELSE GenerateProtocolError[stp, eocExpected, mark] }]; string _ MakeRemoteName[stp.plist]; [answer: reply] _ confirm[string]; IF reply = abort THEN RETURN[aborted]; ENDLOOP; ErrorIfNextNotEOC[stp]; RETURN[finished]; }; no => { code: CHAR = CollectCode[stp]; errorCode: STP.ErrorCode = ErrorCodeToSTPErrorCode[requestRefused, code]; CollectString[stp]; ErrorIfNextNotEOC[stp]; SELECT errorCode FROM protocolError, requestRefused => RETURN[tryOldDirectory]; ENDCASE; IF LOOPHOLE[code, Reply] = badCommand THEN RETURN[tryOldDirectory]; GenerateError[stp, errorCode, code]; }; ENDCASE => GenerateProtocolError[stp, badMark, mark, 0C]; ENDLOOP; }; GetFile: PROC [stp: Handle, stream: IO.STREAM] RETURNS [gotIt: BOOLEAN, code: CHAR] = { mark: Mark; CheckConnection[stp]; SELECT (mark _ MyGetMark[stp]) FROM hereIsFile => NULL; no => { code _ CollectCode[stp]; CollectString[stp]; gotIt _ FALSE; RETURN}; ENDCASE => SelectError[stp, "HereIsFile Expected", mark]; TransferTheFile[stp: stp, from: stp.byteStream, to: stream]; stp.mark _ VAL[PupStream.ConsumeMark[stp.byteStream]]; stp.gotMark _ TRUE; ErrorIfNextNotYes[stp]; -- should do something about file on local disk gotIt _ TRUE; RETURN }; PutFile: PROC [stp: Handle, stream: IO.STREAM] = { CheckConnection[stp]; SendMark[stp.byteStream, hereIsFile]; TransferTheFile[stp: stp, from: stream, to: stp.byteStream]; PutCommand[stp, yes, 0C, "Transfer Completed", TRUE]; }; TransferTheFile: PROC [stp: Handle, from, to: IO.STREAM] = { buffer: REF TEXT = RefText.ObtainScratch[512]; DO nBytes: NAT = IO.GetBlock[from, buffer, 0]; IF nBytes = 0 THEN EXIT; IO.PutBlock[to, buffer, 0, nBytes]; ENDLOOP; RefText.ReleaseScratch[buffer]; }; SmashClosed: PROC [stp: Handle] = { IF stp = NIL OR stp.byteStream = NIL THEN RETURN; PupStream.Abort[stp.byteStream, "Unwinding..."]; Close[stp ! Error => IF code = noConnection THEN CONTINUE]; }; FindFileType: PROC [stream: IO.STREAM] RETURNS [fileType: STP.Type] = { ENABLE IO.Error => IF ec = NotImplementedForThisStream THEN GOTO Unknown; currentIndex: INT = stream.GetIndex[]; buffer: REF TEXT = RefText.ObtainScratch[512]; fileType _ text; DO nBytes: NAT = IO.GetBlock[stream, buffer, 0]; IF nBytes = 0 THEN EXIT; FOR i: NAT IN [0..buffer.length) DO IF buffer[i] > 177C THEN GOTO Binary; ENDLOOP; REPEAT Binary => fileType _ binary; ENDLOOP; RefText.ReleaseScratch[buffer]; stream.SetIndex[currentIndex]; EXITS Unknown => fileType _ unknown; }; remoteStreamProcs: REF IO.StreamProcs = IO.CreateStreamProcs[ variety: $inputOutput, class: $FTP, endOf: EndOf, unsafeGetBlock: UnsafeGetBlock, putChar: PutChar, unsafePutBlock: UnsafePutBlock, close: DeleteRemoteStream]; CreateRemoteStream: PUBLIC PROC [stp: Handle, file: Rope.ROPE, access: STP.Access, fileType: STP.Type, creation: BasicTime.GMT] RETURNS [stream: IO.STREAM] = { rs: RemoteStream = NEW[RemoteObject _ [access: access, state: initial, stp: stp]]; CheckConnection[stp]; IF stp.remoteStream # NIL THEN ERROR Error[stp, accessError, "Remote Stream Exists"]; ResetPList[stp.plist]; UserStateToPList[stp]; NameToPList[stp.plist, file]; SetFileType[stp, fileType]; IF access = write THEN { SetCreateTime[stp, creation]; SetByteSize[stp, fileType]; }; IF fileType = text THEN stp.plist[eolConversion] _ "CR"; stp.remoteStream _ rs; RETURN[IO.CreateStream[remoteStreamProcs, rs]] }; NextFileName: PUBLIC PROC [remoteStream: IO.STREAM] RETURNS [file: Rope.ROPE] = { rs: RemoteStream _ ConvertHandle[remoteStream]; reason: PupStream.CloseReason; IF rs.access # read THEN ERROR Error[rs.stp, accessError, NIL]; { ENABLE PupStream.StreamClosing => {reason _ why; GOTO StreamClosing}; SELECT rs.state FROM initial => RequestRetrieve[rs]; confirm => { PutCommand[rs.stp, no, 0C, "No Thanks"]; IF LookAtMark[rs.stp] = eoc THEN { [] _ MyGetMark[rs.stp]; rs.state _ end; } ELSE GetHereIsAndPList[rs.stp]; }; data => { ErrorIfNextNotYes[rs.stp]; IF LookAtMark[rs.stp] = eoc THEN { [] _ MyGetMark[rs.stp]; rs.state _ end; } ELSE { GetHereIsAndPList[rs.stp]; rs.state _ confirm; }; }; complete => IF LookAtMark[rs.stp] = eoc THEN { [] _ MyGetMark[rs.stp]; rs.state _ end; } ELSE { GetHereIsAndPList[rs.stp]; rs.state _ confirm; }; end => NULL; ENDCASE; EXITS StreamClosing => GenerateStreamClosingError[rs.stp, reason]; }; RETURN[IF rs.state = end THEN NIL ELSE MakeRemoteName[rs.stp.plist]] }; ConvertHandle: PROC [h: IO.STREAM] RETURNS [rs: RemoteStream] = INLINE { RETURN[NARROW[h.streamData]] }; UnsafeGetBlock: UNSAFE PROC[self: IO.STREAM, block: IO.UnsafeBlock] RETURNS [nBytesRead: INT] = UNCHECKED { rs: RemoteStream _ ConvertHandle[self]; reason: PupStream.CloseReason; { ENABLE PupStream.StreamClosing => {reason _ why; GOTO StreamClosing}; IF rs.state # data THEN { StartRemote[rs, read]; IF rs.state = end THEN RETURN[0]; }; nBytesRead _ rs.stp.byteStream.UnsafeGetBlock[block]; IF rs.stp.byteStream.EndOf[] THEN { rs.stp.mark _ VAL[PupStream.ConsumeMark[rs.stp.byteStream]]; rs.stp.gotMark _ TRUE; SetupForNextOrEnd[rs.stp]; }; EXITS StreamClosing => { rs.state _ end; GenerateStreamClosingError[rs.stp, reason]; }; }; }; EndOf: PROC[self: IO.STREAM] RETURNS [BOOL] = { rs: RemoteStream _ ConvertHandle[self]; RETURN[rs.state # data OR rs.stp.byteStream.EndOf[]] }; PutChar: PROC[self: IO.STREAM, char: CHAR] = { rs: RemoteStream _ ConvertHandle[self]; reason: PupStream.CloseReason; { ENABLE PupStream.StreamClosing => {reason _ why; GOTO StreamClosing}; IF rs.state # data THEN StartRemote[rs, write]; rs.stp.byteStream.PutChar[char]; EXITS StreamClosing => {rs.state _ end; GenerateStreamClosingError[rs.stp, reason]}; }; }; UnsafePutBlock: PROC [self: IO.STREAM, block: IO.UnsafeBlock] = { rs: RemoteStream _ ConvertHandle[self]; reason: PupStream.CloseReason; { ENABLE PupStream.StreamClosing => {reason _ why; GOTO StreamClosing}; IF rs.state # data THEN StartRemote[rs, write]; rs.stp.byteStream.UnsafePutBlock[block]; EXITS StreamClosing => {rs.state _ end; GenerateStreamClosingError[rs.stp, reason]}; }; }; DeleteRemoteStream: PROC [self: IO.STREAM, abort: BOOL _ FALSE] = { rs: RemoteStream _ ConvertHandle[self]; stp: Handle = rs.stp; { ENABLE UNWIND => stp.remoteStream _ NIL; IF rs.access = read THEN {IF rs.state # end THEN SmashClosed[stp]} -- in mid-transfer ELSE SELECT rs.state FROM initial => NULL; data => { PutCommand[rs.stp, yes, 0C, "Transfer Completed" ! PupStream.StreamClosing => CONTINUE]; -- OK, if closed in advance ErrorIfNextNotYes[rs.stp]; ErrorIfNextNotEOC[rs.stp]}; confirm, complete, end => NULL; -- worry about later ENDCASE; stp.remoteStream _ NIL;}; }; SetupForNextOrEnd: PROC [stp: Handle] = { ErrorIfNextNotYes[stp]; SELECT LookAtMark[stp] FROM eoc => {[] _ MyGetMark[stp]; stp.remoteStream.state _ end}; hereIsPList => stp.remoteStream.state _ complete; ENDCASE => GenerateProtocolError[stp, badMark, MyGetMark[stp]]; }; RequestRetrieve: PROC [rs: RemoteStream] = { PutPList[rs.stp, retrieve]; GetHereIsAndPList[rs.stp ! Error => SELECT code FROM IN STP.CredentialsErrors, IN STP.FileErrors => rs.state _ end; ENDCASE]; rs.state _ confirm; }; StartRemote: PROC [rs: RemoteStream, access: STP.Access] = { reason: PupStream.CloseReason; IF rs.access # access THEN ERROR Error[rs.stp, accessError, "Remote stream access Error"]; { ENABLE PupStream.StreamClosing => {reason _ why; GOTO StreamClosing}; SELECT access FROM read => { IF rs.state = initial THEN RequestRetrieve[rs]; IF rs.state = complete THEN { IF LookAtMark[rs.stp] = eoc THEN {rs.state _ end; RETURN} ELSE GetHereIsAndPList[rs.stp]; rs.state _ confirm; }; IF rs.state = confirm THEN { mark: Mark; PutCommand[rs.stp, yes, 0C, "Yes, please"]; SELECT (mark _ MyGetMark[rs.stp]) FROM hereIsFile => NULL; no => { rs.state _ complete; SelectError[rs.stp, "He says NO", mark]; }; ENDCASE => SelectError[rs.stp, "HereIsFile Expected", mark]; }; }; write => { PutPList[rs.stp, newStore]; GetHereIsAndPList[rs.stp]; SendMark[rs.stp.byteStream, hereIsFile]; }; ENDCASE => ERROR; rs.state _ data; EXITS StreamClosing => GenerateStreamClosingError[rs.stp, reason]; }; }; Connect: PUBLIC PROC [stp: Handle, name, password: Rope.ROPE] = { stp.userState[connectName] _ Replace[stp.userState[connectName], name]; stp.userState[connectPassword] _ Replace[stp.userState[connectPassword], password]; }; GetProperty: PUBLIC PROC [stp: Handle, prop: ValidProperties] RETURNS [Rope.ROPE] = { RETURN[Rope.FromRefText[stp.plist[prop]]]; }; Login: PUBLIC PROC [stp: Handle, name, password: Rope.ROPE] = { stp.userState[userName] _ Replace[stp.userState[userName], name]; stp.userState[userPassword] _ Replace[stp.userState[userPassword], password]; }; SetDirectory: PUBLIC PROC [stp: Handle, directory: Rope.ROPE] = { stp.userState[directory] _ Replace[stp.userState[directory], directory]; }; Replace: PROC [text: REF TEXT, rope: Rope.ROPE] RETURNS [new: REF TEXT] = { text.length _ 0; new _ RefText.AppendRope[text, rope]; }; GetCommand: PROC [stp: Handle] RETURNS [mark: Mark, code: CHAR] = { code _ 0C; CheckConnection[stp]; mark _ MyGetMark[stp]; SELECT mark FROM abort, comment => CollectString[stp]; iAmVersion, no, yes => { code _ CollectCode[stp]; CollectString[stp]; }; ENDCASE => GenerateProtocolError[stp, badMark, mark, code]; }; GetHereIsAndPList: PROC [stp: Handle, gobbleEOC: BOOL _ TRUE] = { mark: Mark; DO SELECT (mark _ MyGetMark[stp]) FROM comment => CollectString[stp]; hereIsPList => {GetPList[stp, gobbleEOC, FALSE]; EXIT}; no => { code: CHAR = CollectCode[stp]; errorCode: STP.ErrorCode = ErrorCodeToSTPErrorCode[requestRefused, code]; CollectString[stp]; ErrorIfNextNotEOC[stp]; GenerateError[stp, errorCode, code]; }; ENDCASE => GenerateProtocolError[stp, badMark, mark, 0C]; ENDLOOP; }; GetPList: PROC [stp: Handle, gobbleEOC: BOOL, propertiesOk: BOOL] = { property: REF TEXT _ RefText.ObtainScratch[512]; value: REF TEXT _ RefText.ObtainScratch[512]; Append: PROC [c: CHAR] = { SELECT which FROM property => property _ RefText.AppendChar[property, c]; value => value _ RefText.AppendChar[value, c]; ENDCASE; }; parens: INT _ 0; char: CHAR; which: {property, value} _ property; CheckConnection[stp]; stp.plist[nameBody].length _ 0; DO char _ MyGetChar[stp ! MarkEncountered => IF NOT propertiesOk THEN GOTO BadPList]; SELECT char FROM '' => Append[MyGetChar[stp ! MarkEncountered => GOTO BadPList]]; '( => { parens _ parens + 1; which _ property; property.length _ 0; }; ' => IF which = property THEN { which _ value; value.length _ 0; } ELSE Append[char]; ') => { IF property.length # 0 AND value.length # 0 THEN { SetPListItem[ stp.plist, RefText.TrustTextAsRope[property], RefText.TrustTextAsRope[value] ! BadProperty => CONTINUE]; property.length _ 0; value.length _ 0; }; IF (parens _ parens - 1) = 0 THEN EXIT; }; ENDCASE => Append[char]; ENDLOOP; IF (property.length # 0 AND value.length # 0) THEN GOTO BadPList; IF gobbleEOC AND MyGetMark[stp] # eoc THEN GOTO BadPList; RefText.ReleaseScratch[property]; RefText.ReleaseScratch[value]; EXITS BadPList => {ResetPList[stp.plist]; GenerateProtocolError[stp, badPList, stp.mark]}; }; SendMark: PROC [stream: IO.STREAM, mark: Mark] = { PupStream.SendMark[stream, mark.ORD]; }; PutCommand: PROC [stp: Handle, mark: Mark, code: CHAR, string: Rope.ROPE, sendEOC: BOOL _ TRUE] = { CheckConnection[stp]; SendMark[stp.byteStream, mark]; stp.byteStream.PutChar[code]; IF string.Length[] # 0 THEN MyPutString[stp.byteStream, string]; IF sendEOC THEN SendMark[stp.byteStream, eoc]; }; PutPList: PROC [stp: Handle, mark: Mark, sendEOC: BOOL _ TRUE] = { state: {okay, tryOpen, triedOpen} _ okay; DoIt: PROC = { CheckConnection[stp]; IF mark # null THEN SendMark[stp.byteStream, mark]; stp.byteStream.PutChar['(]; FOR i: ValidProperties IN ValidProperties DO IF stp.plist[i] # NIL AND stp.plist[i].length # 0 THEN PutPListItem[stp.byteStream, i, stp.plist[i]]; ENDLOOP; IF stp.desiredProps # ALL[TRUE] THEN PutDesiredProps[stp]; stp.byteStream.PutChar[')]; IF sendEOC THEN SendMark[stp.byteStream, eoc]; }; DO DoIt[! PupStream.StreamClosing, PupStream.Timeout => IF state = okay AND stp.host.Length[] # 0 THEN {state _ tryOpen; CONTINUE}]; IF state # tryOpen THEN EXIT; { savedPList: PList _ stp.plist; stp.plist _ NIL; SmashClosed[stp]; -- Call outside catch so Pup monitor unlocked [] _ STP.Open[stp, stp.host]; stp.plist _ savedPList; state _ triedOpen; }; ENDLOOP; }; CollectString: PROC [stp: Handle] = { text: REF TEXT _ RefText.ObtainScratch[512]; stp.remoteString.length _ 0; DO text.length _ 0; IF stp.byteStream.GetBlock[text] = 0 THEN EXIT; stp.remoteString _ RefText.Append[stp.remoteString, text]; ENDLOOP; stp.mark _ VAL[PupStream.ConsumeMark[stp.byteStream]]; stp.gotMark _ TRUE; SELECT stp.mark FROM hereIsPList, eoc => NULL; ENDCASE => GenerateProtocolError[stp, eocExpected, stp.mark]; RefText.ReleaseScratch[text]; }; CollectCode: PROC [stp: Handle] RETURNS [code: CHAR] = { code _ 0C; CheckConnection[stp]; code _ MyGetChar[stp ! MarkEncountered => GenerateProtocolError[stp, noCode, MyGetMark[stp]]]; }; CheckConnection: PROC [stp: Handle] = { WHILE stp.byteStream = NIL DO SIGNAL Error[stp, noConnection, "Please open a connection!", 0C] ENDLOOP }; ErrorIfNextNotYes: PROC [stp: Handle] = { code: CHAR _ 0C; mark: Mark; [mark, code] _ GetCommand[stp]; IF mark # yes THEN GenerateError[stp, requestRefused, code]; }; ErrorIfNextNotEOC: PROC [stp: Handle] = { mark: Mark _ MyGetMark[stp]; IF mark # eoc THEN GenerateProtocolError[stp, eocExpected, mark]; }; LookAtMark: PROC [stp: Handle] RETURNS [Mark] = { IF ~stp.gotMark THEN DO [] _ MyGetChar[stp ! MarkEncountered => CONTINUE]; IF stp.gotMark THEN EXIT; ENDLOOP; RETURN[stp.mark] }; MyGetChar: PUBLIC PROC [stp: Handle] RETURNS [char: CHAR] = { char _ IO.GetChar[stp.byteStream ! IO.EndOfStream => GOTO End]; EXITS End => { stp.mark _ VAL[PupStream.ConsumeMark[stp.byteStream]]; stp.gotMark _ TRUE; ERROR MarkEncountered; } }; MyGetMark: PUBLIC PROC [stp: Handle] RETURNS [mark: Mark] = { mark _ LookAtMark[stp]; stp.gotMark _ FALSE; RETURN[mark]; }; MyPutString: PUBLIC PROC [byteStream: IO.STREAM, string: Rope.ROPE] = { byteStream.PutRope[string]; }; MyPutBlock: PUBLIC PROC [byteStream: IO.STREAM, string: REF TEXT] = { byteStream.PutBlock[string]; }; MyPutStringVal: PUBLIC PROC [byteStream: IO.STREAM, string: REF TEXT] = { FOR i: INT IN [0..string.length) DO char: CHAR = string[i]; SELECT char FROM '(, '), '' => byteStream.PutChar[''] ENDCASE => NULL; byteStream.PutChar[char]; ENDLOOP; }; SetCreateTime: PUBLIC PROC [stp: Handle, creation: BasicTime.GMT] = { month: ARRAY BasicTime.MonthOfYear OF Rope.ROPE = [ January: "Jan", February: "Feb", March: "Mar", April: "Apr", May: "May", June: "Jun", July: "Jul", August: "Aug", September: "Sep", October: "Oct", November: "Nov", December: "Dec" ]; zoneIndex: TYPE = [4 .. 10]; zoneChars: ARRAY zoneIndex OF CHAR = ['A, 'E, 'C, 'M, 'P, 'Y, 'H]; unpack: BasicTime.Unpacked = BasicTime.Unpack[creation]; absZone: INT _ ABS[IF unpack.dst = yes THEN unpack.zone - 60 ELSE unpack.zone]; str: IO.STREAM = IO.ROS[]; str.PutF["%02d-%g-%02d", [integer[unpack.day]], [rope[month[unpack.month]]], [integer[unpack.year MOD 100]] ]; str.PutF[" %02d:%02d:%02d ", [integer[unpack.hour]], [integer[unpack.minute]], [integer[unpack.second]] ]; IF (unpack.zone / 60 IN zoneIndex) AND absZone MOD 60 = 0 THEN { str.PutChar[zoneChars[unpack.zone / 60]]; str.PutChar[IF unpack.dst = yes THEN 'D ELSE 'S]; str.PutChar['T]; } ELSE str.PutF["%g%g:%02d", [character[IF unpack.zone < 0 THEN '- ELSE '+]], [integer[absZone/60]], [integer[absZone MOD 60]] ]; stp.plist[createDate] _ Replace[stp.plist[createDate], str.RopeFromROS[]]; }; SetFileType: PUBLIC PROC [stp: Handle, fileType: STP.Type] = { text: Rope.ROPE _ SELECT fileType FROM text => "Text", binary => "Binary", ENDCASE => NIL; stp.plist[type] _ Replace[stp.plist[type], text]; }; SetByteSize: PUBLIC PROC [stp: Handle, fileType: STP.Type] = { text: Rope.ROPE _ IF fileType = text THEN NIL ELSE "8"; stp.plist[byteSize] _ Replace[stp.plist[byteSize], text]; }; StringToFileSize: PUBLIC PROC [string: REF TEXT] RETURNS [INT] = { IF RefText.Length[string] = 0 THEN RETURN[0]; RETURN[Convert.IntFromRope[RefText.TrustTextAsRope[string]]]; }; StringToFileType: PUBLIC PROC [string: REF TEXT] RETURNS [type: STP.Type] = { type _ SELECT TRUE FROM RefText.Equal["Text", string, FALSE] => text, RefText.Equal["Binary", string, FALSE] => binary, ENDCASE => unknown; }; NameToPList: PUBLIC PROC [plist: PList, name: Rope.ROPE] = { pos: INT _ 0; length: INT = Rope.Length[name]; IF pos >= length THEN RETURN; IF Rope.Fetch[name, pos] = '[ THEN { end: INT = Rope.Find[name, "]"]; IF end > pos THEN { plist[device].length _ 0; FOR i: INT IN (pos..end) DO plist[device] _ RefText.AppendChar[plist[device], Rope.Fetch[name, i] ]; ENDLOOP; pos _ end+1 }; IF pos >= length THEN RETURN; }; IF Rope.Fetch[name, pos] = '< THEN { end: INT = Rope.Find[name, ">"]; IF end > pos THEN { plist[directory].length _ 0; FOR i: INT IN (pos..end) DO plist[directory] _ RefText.AppendChar[plist[directory], Rope.Fetch[name, i] ]; ENDLOOP; pos _ end+1 }; IF pos >= length THEN RETURN; }; { end: INT _ Rope.Find[name, "!"]; IF end >= 0 THEN { plist[version].length _ 0; FOR i: INT IN (end..length) DO plist[version] _ RefText.AppendChar[plist[version], Rope.Fetch[name, i] ]; ENDLOOP; } ELSE end _ length; plist[nameBody].length _ 0; FOR i: INT IN [pos..end) DO plist[nameBody] _ RefText.AppendChar[plist[nameBody], Rope.Fetch[name, i] ]; ENDLOOP; }; }; PListToName: PUBLIC PROC [plist: PList] RETURNS [name: Rope.ROPE _ NIL] = { text: REF TEXT _ RefText.ObtainScratch[512]; dir: REF TEXT = plist[directory]; dirLength: INT = RefText.Length[dir]; IF dirLength # 0 THEN { SELECT dir[0] FROM '> => FOR i: INT IN [1..dirLength) DO text _ RefText.AppendChar[text, dir[i]]; ENDLOOP; '< => text _ RefText.Append[text, dir]; ENDCASE => { text _ RefText.AppendChar[text, '<]; text _ RefText.Append[text, dir]; }; IF dir[dirLength - 1] # '> THEN text _ RefText.AppendChar[text, '>]; }; IF plist[nameBody].length # 0 THEN text _ RefText.Append[text, plist[nameBody]]; IF plist[version].length # 0 THEN { text _ RefText.AppendChar[text, '!]; text _ RefText.Append[text, plist[version]]; }; name _ Rope.FromRefText[text]; RefText.ReleaseScratch[text]; }; MakeRemoteName: PUBLIC PROC [plist: PList] RETURNS [Rope.ROPE] = { RETURN[IF RefText.Length[plist[serverName]] = 0 THEN PListToName[plist] ELSE Rope.FromRefText[plist[serverName]] ]}; PutPListItem: PROC [byteStream: IO.STREAM, property: ValidProperties, value: REF TEXT] = { byteStream.PutChar['(]; MyPutBlock[byteStream, propertyStrings[property]]; byteStream.PutChar[' ]; MyPutStringVal[byteStream, value]; byteStream.PutChar[')]; }; ResetPList: PUBLIC PROC [plist: PList] = { IF plist = NIL THEN RETURN; FOR p: ValidProperties IN ValidProperties DO plist[p].length _ 0; ENDLOOP; }; GetFileInfo: PUBLIC PROC [stp: Handle] RETURNS [STP.FileInfo] = { stp.info^ _ [ directory: Rope.FromRefText[stp.plist[directory]], body: Rope.FromRefText[stp.plist[nameBody]], version: Rope.FromRefText[stp.plist[version]], author: Rope.FromRefText[stp.plist[author]], create: Rope.FromRefText[stp.plist[createDate]], read: Rope.FromRefText[stp.plist[readDate]], write: Rope.FromRefText[stp.plist[writeDate]], size: StringToFileSize[stp.plist[size]], type: StringToFileType[stp.plist[type]] ]; RETURN[stp.info]; }; SetPListItem: PUBLIC PROC [plist: PList, property, value: Rope.ROPE] = { FOR i: ValidProperties IN ValidProperties DO IF Rope.Equal[property, RefText.TrustTextAsRope[propertyStrings[i]], FALSE] THEN { plist[i] _ Replace[plist[i], value]; RETURN; }; ENDLOOP; ERROR BadProperty }; UserStateToPList: PUBLIC PROC [stp: Handle] = { FOR i: UserProperties IN UserProperties DO stp.plist[i].length _ 0; stp.plist[i] _ RefText.Append[stp.plist[i], stp.userState[i]]; ENDLOOP; }; SetDesiredProperties: PUBLIC PROC[stp: Handle, props: STP.DesiredProperties] = { stp.desiredProps _ props }; GetDesiredProperties: PUBLIC PROC [stp: Handle] RETURNS [props: STP.DesiredProperties] = { RETURN[stp.desiredProps] }; PutDesiredProps: PUBLIC PROC [stp: Handle] = { FOR i: FileProperties IN FileProperties DO IF stp.desiredProps[i] THEN PutDesiredPropsItem[stp.byteStream, propertyStrings[i]] ENDLOOP; }; PutDesiredPropsItem: PUBLIC PROC [byteStream: IO.STREAM, value: REF TEXT] = { byteStream.PutChar['(]; MyPutBlock[byteStream, desiredPropString]; byteStream.PutChar[' ]; MyPutBlock[byteStream, value]; byteStream.PutChar[')]; }; }. STPImpl.mesa - Simple/Stream Transfer Protocol Copyright c 1985, 1986 by Xerox Corporation. All rights reserved. Evans, Nov 14, 1980 9:35 AM Mark, Feb 12, 1981 11:39 PM Smokey, 17-Jul-81 7:48:31 JGS, 17-Aug-81 11:26:24 Sandman, 17-Aug-81 11:32:55 SHayes, 11-Sep-81 16:15:34 Bruce, 20-Oct-81 11:24:17 Johnsson, 16-Dec-81 17:39:42 Karlton, 15-Mar-82 17:26:37 Daniels, 21-Sep-82 13:05:55 Loretta, 27-Oct-82 10:45:58 Davirro, 15-Dec-82 15:32:52 Schmidt, January 20, 1983 7:10 pm Andrew Birrell, June 1, 1983 5:39 pm Levin, August 9, 1983 9:59 am Schroeder, August 10, 1983 5:56 pm MBrown, September 17, 1983 8:14 pm HGM, February 20, 1984 10:29:03 pm PST Russ Atkinson (RRA) April 4, 1985 11:40:25 am PST Bob Hagmann May 9, 1986 1:51:17 pm PDT Hal Murray, June 5, 1986 3:30:26 pm PDT Doug Wyatt, May 16, 1986 3:00:32 pm PDT Tim Diebert: May 19, 1986 10:22:53 am PDT Errors Global Data Public Interface Routines Error generation routines Procedures for normal FTP-like interface common routine for Delete, Enumerate and Retrieve RRA: sometimes the new directory sends so much stuff that poor old servers can't handle it, so we try to use the old protocol. Procedures for doing FTP protocol operations First smash connection so it will not give us any grief, THEN close it Procedures implementing STP interface Procedures for Remote streams Utilities Commonly used stuff Procedures for doing FTP protocol operations Utility routines Desired syntax is: "dd-mmm-yy hh:mm:ss zzz" PList and FileInfo Utilities Desired Property stuff Bob Hagmann May 9, 1986 1:44:55 pm PDT merged STPsA, STPsB, STPsC, and STPsD Hal Murray, May 12, 1986 5:46:15 pm PDT Pruned many many things out of STPOps Tim Diebert, May 19, 1986 10:20:30 am PDT Put some STPOps stuff in STPBackdoor. Κ(,˜codešœ.™.Kšœ Οmœ7™BKšœ™Kšœ™Kšœ!™!Kšœ&™&Kšœ™Kšœ™Kšœ™Kšœ™Kšœ"™"Kšœ"™"Kšœ™Kšœ™Kšœ!™!K™$K™K™"K™"K™&K™1K™&K™'K™'K™)—K˜šΟk ˜ Kšœ žœžœ!˜5Kšœžœ˜Kšžœžœ¨žœ žœ:˜Kšœžœ ˜Kšœžœ˜"Kšœ žœZ˜iKšœžœ˜Kšœžœa˜nKšœžœ+žœ˜;KšžœžœΟ˜ΨK˜ K˜ K˜—šΟnœžœž˜Kšžœ žœžœ%ž˜FKšžœžœžœ ˜-K˜Kšœ žœžœ ˜ Kšžœžœžœ˜K˜KšœžœžœΟc˜9K˜—™K˜Kš œžœžœ'žœ žœ žœ˜ZK˜—Kšœ ™ ™KšŸœžœžœ˜KšŸ œžœžœžœ˜!šœ˜K˜K˜K˜K˜$K˜K˜ K˜ K˜K˜K˜K˜K˜K˜K˜K˜(K˜K˜K˜K˜K˜—Kšœžœžœ˜1K˜K˜—šœ™K˜šŸœžœžœžœ˜$Kšœžœ˜Kšžœžœžœžœ˜Kšœ!žœ˜%K˜šžœž˜KšžœQ˜V—Kšœ˜K˜—šŸ œžœžœ˜%Kšœ˜Kš žœžœžœžœ)žœ˜QKšœ˜K˜—š Ÿœžœžœžœžœ˜-Kšœžœ˜Kšœ žœžœ˜(Kšœžœžœ˜"Kšœ žœ ˜šžœžœž˜,Kšœžœžœ˜Kšžœ˜—šžœžœž˜*Kšœžœžœ˜"Kšžœ˜—Kšžœ˜ Kšœ˜K˜—š Ÿœžœžœžœ žœ˜7Kš žœžœžœžœžœ˜Kšžœžœžœ ˜(Kšžœžœ˜ Kšœ˜K˜—š Ÿœžœžœžœžœ˜9Kš žœžœžœžœžœ˜+Kšœžœ˜ Kšœžœ;žœžœ˜WKšœ˜K˜—šŸœžœžœžœžœ žœžœ˜FK˜Kšžœžœžœžœ˜/šžœ ž˜KšžœB˜G—Kšžœžœžœ˜0šœ˜Kšžœ+žœ˜E˜;K˜=—Kšœ žœ˜KšœX˜XKšœžœ˜Kšœ/˜/šœ˜Kšœžœ˜ K˜ Kšœ˜Kšžœžœ4˜MKšœ˜Kšœ,˜,—Kšœ˜Kšœ˜KšžœP˜U—Kšœ˜Kšœ˜K˜——šœ™K˜šŸœžœžœžœ˜XKšœžœ˜"šžœžœ ž˜K˜K˜K˜K˜ K˜K˜)K˜$K˜#K˜"K˜K˜$K˜$K˜#K˜+K˜)K˜)K˜1K˜'K˜$K˜#K˜!K˜!K˜K˜K˜K˜Kšœ˜K˜K˜K˜K˜Kšœ˜Kšžœ œ˜1K˜——šŸ œžœ+žœžœ˜LKšœžœžœ˜Kšžœ&žœ-˜YK˜2Kšœ˜K˜—š Ÿœžœ-žœžœžœ˜`šžœ ˜K˜)Kšžœžœ˜&šž˜šžœ ž˜K˜K˜+K˜?K˜7K˜,K˜;K˜šžœ žœ˜K˜ Kšœžœ˜ Kšœ˜Kšžœ žœ*˜;—K˜—Kšœžœ˜ Kšžœžœ˜—Kšœ˜Kšžœ˜—Kšžœ;˜@šœ˜K˜——šŸœžœžœž œ*˜tK˜ Kšœ˜šž˜Kšžœž˜#Kšœ˜šœ˜Kšœ žœ˜Kšœžœ˜šž˜šœžœž˜7šœ˜Kšœ˜Kšžœ žœž˜Kšžœ.˜2K˜——Kšœ#˜#K˜"Kšžœžœžœ ˜&Kšžœ˜—Kšœ˜Kšžœ ˜K˜—šœ˜Kšœžœ˜Kšœ žœ<˜JK˜K˜šžœ ž˜šœ!žœ˜9Kšœ~™~—Kšžœ˜—šžœžœž˜*Kšžœ˜—K˜$Kšœ˜—Kšžœ2˜9Kšžœ˜—K˜K˜——šœ.™.K˜šŸœžœžœžœž œžœžœžœ˜WK˜ K˜šžœž˜#Kšœžœ˜˜Kšœ˜Kšœ˜Kšœžœ˜Kšžœ˜—Kšžœ2˜9—K˜Kšžœ˜ ——K˜Kšœ˜K˜—šŸ œžœžœ ˜Kš œ žœžœ žœ%žœžœ˜ZKšœ1˜1Kšœ˜K˜—šŸ œžœžœžœ ˜>Kš œ žœžœžœžœžœ˜7Kšœ9˜9Kšœ˜K˜—šŸœžœžœ žœžœžœžœ˜BKšžœžœžœ˜-Kšžœ7˜=Kšœ˜K˜—šŸœžœžœ žœžœžœžœ ˜Mšœžœžœž˜Kšœžœ ˜-Kšœ žœ ˜1Kšžœ ˜—Kšœ˜K˜—Kšœ™K˜šŸ œžœžœžœ˜˜>Kšžœ˜—Kšœ˜K˜—Kšœ™K˜šŸœžœžœžœ˜PK˜Kšœ˜K˜—š Ÿœžœžœžœ žœ˜ZKšžœ˜Kšœ˜K˜—šŸœžœžœ˜.šžœžœž˜*Kšžœžœ8˜SKšžœ˜—Kšœ˜K˜—šŸœžœžœžœžœ žœžœ˜MK˜K˜*Kšœ˜K˜K˜Kšœ˜K˜—Kšœ˜K˜K˜—K˜K˜™&Kšœ%™%—™'K™%—K™O—…—‚@±„