DIRECTORY Ascii USING [Digit, Letter], Atom USING [GetPropFromList, PutPropOnList], Basics USING [bytesPerWord, LongNumber, SwapHalves], BasicTime USING [GMT, MonthOfYear, Now, nullGMT, Period, ToPupTime, Unpack, Unpacked], CedarProcess USING [Abort, CheckAbort, Fork, ForkableProc, Process], Commander USING [CommandProc, Register], CommandTool USING [ArgumentVector, Failed, Parse], Convert USING [Error, IntFromRope, RopeFromInt], File USING [GetVolumeName, SystemVolume], FS USING [Close, ComponentPositions, Delete, EnumerateForInfo, EnumerateForNames, Error, ErrorGroup, ExpandName, FileInfo, GetInfo, GetName, OpenFile, OpenFileFromStream, Rename, SetByteCountAndCreatedTime, StreamOpen], FSRemoteFile USING [FTPTimeToGMT], GVBasics USING [MakeKey, Password], GVNames USING [AuthenticateKey, IsMemberClosure], Icons USING [DrawIcon, IconFileFormat, IconFlavor, iconH, IconRef, IconRep, iconW, NewIcon], Imager USING [Context, MaskBox, SetColor], ImagerBackdoor USING [invert], IO USING [Backup, Close, EndOf, EndOfStream, Error, GetBlock, GetChar, GetLength, PutBlock, PutChar, PutF, PutF1, PutRope, RIS, RopeFromROS, ROS, SetIndex, STREAM, UnsafeGetBlock], List USING [LORA, Reverse], Process USING [MsecToTicks, Pause], PupDefs USING [GetHopsToNetwork, GetHostName, GetPupContentsBytes, PupBuffer, PupSocket, PupSocketDestroy, PupSocketMake, ReturnPup, veryLongWait], PupStream USING [CloseReason, ConsumeMark, CreatePupByteStreamListener, DestroyPupListener, PupListener, SecondsToTocks, SendMark, StreamClosing, TimeOut], PupTypes USING [fillInPupAddress, ftpSoc, Pair, PupAddress, PupSocketID, PupType], Real USING [RoundLI], RefText USING [AppendChar, ObtainScratch, ReleaseScratch, TrustTextAsRope], Rope USING [Cat, Concat, Equal, Fetch, Find, Flatten, FromRefText, Length, Replace, ROPE, SkipTo, Substr], RopeFile USING [Create], RuntimeError USING [UNCAUGHT], STP USING [ValidProperties], STPOps USING [markComment, markDelete, markDirectory, markEOC, markHereIsFile, markHereIsPList, markIAmVersion, markNewDirectory, markNewStore, markNo, markRetrieve, markStore, markYes], STPReplyCode USING [ReplyCode], SymTab USING [Create, Delete, Fetch, Ref, Store], TiogaOps USING [GetSelection], TypeScript USING [Create], UserCredentials USING [Get], ViewerClasses USING [PaintProc, Viewer, ViewerClass, ViewerClassRec], ViewerIO USING [CreateViewerStreams, GetViewerFromStream], ViewerOps USING [AddProp, DestroyViewer, FetchProp, FetchViewerClass, PaintViewer]; STPServerImpl: CEDAR MONITOR IMPORTS Ascii, Atom, Basics, BasicTime, Commander, CommandTool, Convert, CedarProcess, File, FS, FSRemoteFile, Imager, ImagerBackdoor, GVBasics, GVNames, Icons, IO, List, Process, PupDefs, PupStream, Real, RefText, Rope, RopeFile, RuntimeError, SymTab, TiogaOps, TypeScript, UserCredentials, ViewerIO, ViewerOps ~ BEGIN heraldMessage: ROPE _ "Cedar STP Server of August 22, 1985 3:31:09 pm PDT"; GMT: TYPE ~ BasicTime.GMT; LongNumber: TYPE ~ Basics.LongNumber; LORA: TYPE = LIST OF REF ANY; ROPE: TYPE ~ Rope.ROPE; STREAM: TYPE ~ IO.STREAM; debugging: BOOL _ FALSE; promiscuous: BOOL _ FALSE; useSingle: BOOL _ TRUE; mute: BOOL _ FALSE; UserNameAndPassword: TYPE ~ RECORD [ userName: ROPE, userPassword: GVBasics.Password ]; ValidUser: PROC [userName, password: ROPE, mark: NAT] RETURNS [valid: BOOL _ TRUE] ~ { ENABLE UNWIND => NULL; codedPassword: GVBasics.Password _ GVBasics.MakeKey[password]; authenticated: BOOL _ FALSE; myName, myPassword: ROPE; myLengthToDot, lengthToDot: INT; [myName, myPassword] _ UserCredentials.Get[]; lengthToDot _ userName.Find["."]; myLengthToDot _ myName.Find["."]; IF lengthToDot < 0 AND myLengthToDot > 0 THEN { userName _ userName.Concat[myName.Substr[myLengthToDot]]; }; IF userName.Equal[myName, FALSE] AND password.Equal[myPassword, FALSE] THEN RETURN [TRUE]; SELECT mark FROM STPOps.markRetrieve => RETURN [CanAccess[userName, codedPassword, $read]]; STPOps.markStore, STPOps.markNewStore => RETURN [CanAccess[userName, codedPassword, $create]]; STPOps.markNewDirectory, STPOps.markDirectory => RETURN [CanAccess[userName, codedPassword, NIL]]; ENDCASE => RETURN [CanAccess[userName, codedPassword, $any]]; }; accessCache: SymTab.Ref _ SymTab.Create[127, FALSE]; cacheEntryLife: INT _ 12*LONG[3600]; AccessEntry: TYPE = REF AccessEntryRep; AccessEntryRep: TYPE = RECORD [ codedPassword: GVBasics.Password, lastChecked: BasicTime.GMT _ BasicTime.nullGMT, readFriend: AccessValue _ unknown, createFriend: AccessValue _ unknown, anyFriend: AccessValue _ unknown ]; AccessValue: TYPE = {unknown, yes, no}; CanAccess: PROC [name: ROPE, codedPassword: GVBasics.Password, class: ATOM] RETURNS [BOOL] = { masterName: ROPE _ SELECT class FROM $read => "[]<>STPServer.readAccess", $create => "[]<>STPServer.createAccess", $any => "[]<>STPServer.anyAccess", ENDCASE => NIL; accessEntry: AccessEntry _ NIL; now: BasicTime.GMT _ BasicTime.Now[]; WITH SymTab.Fetch[accessCache, name].val SELECT FROM access: AccessEntry => IF codedPassword = access.codedPassword AND BasicTime.Period[from: access.lastChecked, to: now] < cacheEntryLife THEN accessEntry _ access ELSE [] _ SymTab.Delete[accessCache, name]; ENDCASE; IF accessEntry = NIL THEN { accessEntry _ NEW[AccessEntryRep _ [ codedPassword: codedPassword, lastChecked: now ]]; IF GVNames.AuthenticateKey[name, codedPassword] # individual THEN RETURN [FALSE]; [] _ SymTab.Store[accessCache, name, accessEntry]; }; IF promiscuous THEN RETURN [TRUE]; SELECT class FROM $read, $create, $any => { value: AccessValue _ yes; SELECT class FROM $read => value _ accessEntry.readFriend; $create => value _ accessEntry.createFriend; $any => value _ accessEntry.anyFriend; ENDCASE; SELECT value FROM yes => RETURN [TRUE]; no => RETURN [FALSE]; ENDCASE; }; ENDCASE => RETURN [TRUE]; IF masterName # NIL THEN { friends: ROPE _ RopeFile.Create[masterName ! FS.Error => GO TO bad]; offset: INT _ 0; before, after: CHAR _ ' ; offset _ Rope.Find[friends, name, 0, FALSE]; WHILE offset >= 0 DO IF offset > 0 THEN before _ friends.Fetch[offset-1]; IF offset+name.Length < friends.Length THEN after _ friends.Fetch[offset+name.Length]; IF Ascii.Letter[before] OR Ascii.Digit[before] OR Ascii.Letter[after] OR Ascii.Digit[after] THEN offset _ Rope.Find[friends, name, offset+1, FALSE] ELSE GO TO good; ENDLOOP; offset _ Rope.Find[friends, "^"]; WHILE offset >= 0 DO ropeSize: INT ~ Rope.Length[friends]; size: INT _ 0; UNTIL offset = 0 OR IsDelimiter[Rope.Fetch[friends, offset-1]] DO offset _ offset-1; ENDLOOP; UNTIL offset+size = ropeSize OR IsDelimiter[Rope.Fetch[friends, offset+size]] DO size _ size + 1; ENDLOOP; IF GVNames.IsMemberClosure[Rope.Substr[friends, offset, size], name] = yes THEN GO TO good; offset _ Rope.Find[friends, "^", offset+size]; ENDLOOP; GO TO bad; EXITS bad => SELECT class FROM $read => accessEntry.readFriend _ no; $create => accessEntry.createFriend _ no; $any => accessEntry.anyFriend _ no; ENDCASE; good => SELECT class FROM $read => accessEntry.readFriend _ yes; $create => accessEntry.createFriend _ accessEntry.readFriend _ yes; $any => accessEntry.anyFriend _ accessEntry.createFriend _ accessEntry.readFriend _ yes; ENDCASE; }; RETURN [FALSE]; }; IsDelimiter: PROC [c: CHAR] RETURNS [BOOL] ~ { RETURN [SELECT c FROM ' , '\t, ',, '; , '\n => TRUE, ENDCASE => FALSE] }; HidePassword: PROC [rope: ROPE] RETURNS [ROPE] ~ { i: INT _ rope.Find["Password ", 0, FALSE]; e: INT _ 0; IF i >= 0 THEN i _ i + 9; IF i >= 0 THEN e _ rope.Find[")", i]; IF e > 0 THEN rope _ rope.Replace[i, e-i, Rope.Substr["****************", 0, e-i]]; RETURN [rope] }; monthName: ARRAY BasicTime.MonthOfYear OF ROPE ~ ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "???"]; PutFileDate: PROC [stream: STREAM, date: BasicTime.GMT] ~ { unpacked: BasicTime.Unpacked _ BasicTime.Unpack[date]; zone: CHAR _ ' ; IO.PutF[stream, "%02g-%g-%02g ", [integer[unpacked.day]], [rope[monthName[unpacked.month]]], [integer[unpacked.year MOD 100]]]; IO.PutF[stream, "%02g:%02g:%02g", [integer[unpacked.hour]], [integer[unpacked.minute]], [integer[unpacked.second]]]; zone _ SELECT unpacked.zone/60 FROM 0 => 'G, 5 => 'E, 6 => 'C, 7 => 'M, 8 => 'P, ENDCASE => ' ; IF zone # ' THEN { IO.PutChar[stream, ' ]; IO.PutChar[stream, zone]; IO.PutChar[stream, IF unpacked.dst = yes THEN 'D ELSE 'S]; IO.PutChar[stream, 'T]; }; }; SendPropList: PROC [stream: STREAM, userProps: UserProperties, fullFName: ROPE, created: BasicTime.GMT, bytes: INT] RETURNS [refused: BOOL _ FALSE] ~ { cp: FS.ComponentPositions; desiredProperty: ARRAY STP.ValidProperties OF BOOL _ userProps.desiredProperty; desiredProperty[serverName] _ desiredProperty[directory] _ desiredProperty[nameBody] _ desiredProperty[version] _ desiredProperty[byteSize] _ TRUE; [fullFName, cp] _ FS.ExpandName[fullFName]; IO.PutChar[stream, '(]; IF desiredProperty[serverName] THEN { IO.PutRope[stream, "(Server-Filename <"]; IF cp.dir.length = 0 THEN IO.PutRope[stream, File.GetVolumeName[File.SystemVolume[]]] ELSE IO.PutRope[stream, fullFName.Substr[cp.dir.start, cp.dir.length]]; IO.PutChar[stream, '>]; IO.PutRope[stream, fullFName.Substr[cp.subDirs.start]]; IO.PutChar[stream, ')]; }; IF desiredProperty[directory] THEN { IO.PutRope[stream, "(Directory "]; IF cp.dir.length = 0 THEN IO.PutRope[stream, File.GetVolumeName[File.SystemVolume[]]] ELSE IO.PutRope[stream, fullFName.Substr[cp.dir.start, cp.dir.length]]; IF cp.subDirs.length > 0 THEN IO.PutChar[stream, '>]; IO.PutRope[stream, fullFName.Substr[cp.subDirs.start, cp.subDirs.length]]; IO.PutChar[stream, ')]; }; IF desiredProperty[nameBody] THEN { IO.PutRope[stream, "(Name-Body "]; IO.PutRope[stream, fullFName.Substr[cp.base.start, cp.ext.start+cp.ext.length-cp.base.start]]; IO.PutChar[stream, ')]; }; IF desiredProperty[version] THEN { IO.PutRope[stream, "(Version "]; IO.PutRope[stream, fullFName.Substr[cp.ver.start, cp.ver.length]]; IO.PutChar[stream, ')]; }; IF desiredProperty[createDate] THEN { IO.PutRope[stream, "(Creation-Date "]; PutFileDate[stream, created]; IO.PutChar[stream, ')]; }; IF desiredProperty[byteSize] THEN { IO.PutRope[stream, "(Byte-Size 8)"]; }; IF desiredProperty[type] THEN { IO.PutRope[stream, "(Type Binary)"]; }; IF desiredProperty[size] THEN { IO.PutF1[stream, "(Size %g)", [integer[bytes]]]; }; IO.PutChar[stream, ')]; }; RetrieveFile: PROC [stream: STREAM, userProps: UserProperties, fullFName: ROPE, created: BasicTime.GMT, bytes: INT] RETURNS [refused: BOOL _ FALSE] ~ { expl: ROPE _ NIL; group: FS.ErrorGroup _ environment; cs: CommandString; HereIsPList[stream, userProps, fullFName, created, bytes]; cs _ GetCommandString[stream]; SELECT cs.mark FROM STPOps.markYes => { ENABLE { FS.Error => {group _ error.group; expl _ error.explanation; GO TO bogus}; IO.Error => {expl _ "Error while reading file"; GO TO bogus}; }; local: STREAM; local _ FS.StreamOpen[fullFName, $read, [tiogaRead: FALSE, commitAndReopenTransOnFlush: TRUE, truncatePagesOnClose: TRUE, finishTransOnClose: TRUE, closeFSOpenFileOnClose: TRUE] ]; PupStream.SendMark[stream, STPOps.markHereIsFile]; CopyStream[from: local, to: stream, fileByteSize: local.GetLength]; IO.Close[local]; Yes[stream, "Transfer complete"]; EXITS bogus => GenerateFileError[stream, group, expl] }; STPOps.markNo => refused _ TRUE; ENDCASE; }; DeleteFile: PROC [stream: STREAM, userProps: UserProperties, fullFName: ROPE, created: BasicTime.GMT, bytes: INT] RETURNS [refused: BOOL _ FALSE] ~ { expl: ROPE _ NIL; group: FS.ErrorGroup _ ok; cs: CommandString; HereIsPList[stream, userProps, fullFName, created, bytes]; cs _ GetCommandString[stream]; SELECT cs.mark FROM STPOps.markYes => { FS.Delete[fullFName, created ! FS.Error => {group _ error.group; expl _ error.explanation; GO TO bogus} ]; Yes[stream, "Deleted"]; EXITS bogus => GenerateFileError[stream, group, expl]; }; STPOps.markNo => refused _ TRUE; ENDCASE; }; FullFNameFromUserProperties: PROC [userProps: UserProperties] RETURNS [fullFName: ROPE] ~ { IF userProps.serverName # NIL THEN { sName: ROPE _ userProps.serverName; IF sName.Length > 0 AND sName.Fetch[0] = '< THEN sName _ Rope.Concat["[]", sName]; fullFName _ FS.ExpandName[sName, Rope.Cat["[]<", userProps.directory, ">"]].fullFName } ELSE { s: STREAM _ IO.ROS[]; dlen: INT _ userProps.directory.Length; s.PutRope["[]"]; IF dlen = 0 OR userProps.directory.Fetch[0] # '< THEN s.PutChar['<]; s.PutRope[userProps.directory]; IF dlen = 0 OR userProps.directory.Fetch[dlen-1] # '> THEN s.PutChar['>]; s.PutRope[userProps.nameBody]; fullFName _ s.RopeFromROS; }; }; StoreFile: PROC [stream: STREAM, userProps: UserProperties, cs: CommandString, newStore: BOOL _ TRUE] ~ { local: STREAM _ NIL; expl: ROPE _ NIL; group: FS.ErrorGroup _ environment; createByteCount: INT _ 2560; fullFName: ROPE _ NIL; desiredVersion: INT _ 0; desiredCreate: GMT _ BasicTime.nullGMT; GetUserProperties[cs, userProps]; IF NOT ValidUser[userProps.userName, userProps.userPassword, cs.mark] THEN { No[stream, accessDenied, "Access denied"]; RETURN; }; { fullFName _ FullFNameFromUserProperties[userProps ! FS.Error => {group _ error.group; expl _ error.explanation; GO TO bogus}]; createByteCount _ Convert.IntFromRope[userProps.size ! Convert.Error => CONTINUE]; desiredVersion _ ParseInteger[userProps.version, 0]; IF userProps.createdTime # NIL THEN desiredCreate _ FSRemoteFile.FTPTimeToGMT[userProps.createdTime ! RuntimeError.UNCAUGHT => CONTINUE]; IF fullFName # NIL THEN { local _ FS.StreamOpen[ fileName: fullFName, accessOptions: $create, streamOptions: [tiogaRead: FALSE, commitAndReopenTransOnFlush: TRUE, truncatePagesOnClose: TRUE, finishTransOnClose: TRUE, closeFSOpenFileOnClose: FALSE], keep: 2, createByteCount: createByteCount ! FS.Error => {group _ error.group; expl _ error.explanation; GO TO bogus} ]; }; IF newStore THEN { realName: ROPE _ NIL; sendCreated: GMT; openFile: FS.OpenFile _ FS.OpenFileFromStream[local]; realName _ FS.GetName[openFile].fullFName; sendCreated _ FS.GetInfo[openFile].created; IF desiredVersion = 0 THEN fullFName _ realName; IF desiredCreate # BasicTime.nullGMT THEN sendCreated _ desiredCreate; HereIsPList[stream, userProps, fullFName, sendCreated, createByteCount]; } ELSE Yes[stream, "Isn't it time you implemented the new store protocol?"]; SELECT cs.mark _ PupStream.ConsumeMark[stream] FROM STPOps.markNo => { cs _ GetCommandString[stream: stream, markConsumed: TRUE, mark: cs.mark]; }; STPOps.markHereIsFile => { CopyStream[from: stream, to: local, fileByteSize: createByteCount ! IO.Error => {expl _ "Error while reading file"; GO TO bogus}; ]; cs _ GetCommandString[stream]; SELECT cs.mark FROM STPOps.markYes => { openFile: FS.OpenFile _ FS.OpenFileFromStream[local]; name: ROPE _ FS.GetName[openFile].fullFName; local.Close[ ! IO.Error => { expl _ "Error while reading file"; GO TO bogus} ]; IF desiredCreate # BasicTime.nullGMT AND expl = NIL THEN { FS.SetByteCountAndCreatedTime[file: openFile, created: desiredCreate ! FS.Error => {group _ error.group; expl _ error.explanation; GO TO bogus} ]; }; IF expl = NIL THEN openFile.Close[ ! FS.Error => {group _ error.group; expl _ error.explanation; GO TO bogus} ]; IF expl # NIL THEN {GenerateFileError[stream, group, expl]; RETURN}; Yes[stream, "Transfer Completed"]; IF desiredVersion > 0 THEN IF SetVersion[name, desiredVersion] THEN IO.PutF1[stream, "\000Version set for %g.", [rope[name]]] ELSE IO.PutF[stream, "\000Version not set for %g, %g desired.", [rope[name]], [integer[desiredVersion]]]; }; STPOps.markNo => { openFile: FS.OpenFile _ FS.OpenFileFromStream[local]; name: ROPE _ FS.GetName[openFile].fullFName; created: BasicTime.GMT _ FS.GetInfo[openFile].created; local.Close[abort: TRUE ! IO.Error => {expl _ "Error while reading file"; GO TO bogus}]; FS.Delete[name ! FS.Error => CONTINUE]; No[stream, notCompleted, "Store not completed"]; IF expl # NIL THEN GO TO bogus; }; ENDCASE => SIGNAL ProtocolError; }; ENDCASE => SIGNAL ProtocolError; EXITS bogus => {GenerateFileError[stream, group, expl]; RETURN}; }; }; SetVersion: PROC [name: ROPE, version: INT] RETURNS [ok: BOOL _ FALSE] = { IF CurrentVersion[name] IN [1..version) THEN { sansVersion: ROPE _ Rope.Flatten[name, 0, Rope.SkipTo[name, 0, "!"]]; curName: ROPE _ name; DO FS.Rename[from: curName, to: sansVersion ! FS.Error => EXIT; ]; curName _ FS.FileInfo[name: sansVersion, remoteCheck: FALSE ! FS.Error => EXIT; ].fullFName; IF CurrentVersion[curName] >= version THEN RETURN [TRUE]; ENDLOOP; }; }; CurrentVersion: PROC [name: ROPE] RETURNS [version: INT _ 0] = { len: INT _ Rope.Length[name]; pos: INT _ Rope.SkipTo[name, 0, "!"]; IF pos # len THEN version _ ParseInteger[name, pos+1]; }; ParseInteger: PROC [name: ROPE, pos: INT] RETURNS [n: INT _ 0] = { len: INT _ Rope.Length[name]; FOR i: INT IN [pos..len) DO c: CHAR _ Rope.Fetch[name, i]; IF c IN ['0..'9] THEN n _ n * 10 + (c-'0) ELSE EXIT; IF n > LAST[INTEGER] THEN RETURN [-1]; ENDLOOP; }; GenerateFileError: PROC [stream: STREAM, group: FS.ErrorGroup, expl: ROPE] ~ { No[stream, IF group = lock THEN fileBusy ELSE permanentError, expl]; }; No: PROC [stream: STREAM, replyCode: STPReplyCode.ReplyCode, expl: ROPE] ~ { WITH Atom.GetPropFromList[stream.propList, $STPServerViewerStream] SELECT FROM viewerOut: STREAM => viewerOut.PutF["No (%g), %g\n", [integer[ORD[replyCode]]], [rope[expl]] ! IO.Error => CONTINUE; ]; ENDCASE; PupStream.SendMark[stream, STPOps.markNo]; IO.PutChar[stream, LOOPHOLE[replyCode]]; Finish[stream, expl]; }; Yes: PROC [stream: STREAM, expl: ROPE] ~ { PupStream.SendMark[stream, STPOps.markYes]; IO.PutChar[stream, 0C]; Finish[stream, expl]; }; Finish: PROC [stream: STREAM, expl: ROPE _ NIL] ~ { IF expl # NIL THEN IO.PutRope[stream, expl]; PupStream.SendMark[stream, STPOps.markEOC]; }; HereIsPList: PROC [stream: STREAM, userProps: UserProperties, fullFName: ROPE, created: BasicTime.GMT, bytes: INT] ~ { PupStream.SendMark[stream, STPOps.markHereIsPList]; [] _ SendPropList[stream, userProps, fullFName, created, bytes]; Finish[stream]; }; DoFiles: PROC [stream: STREAM, userProps: UserProperties, cs: CommandString, action: PROC [stream: STREAM, userProps: UserProperties, fullFName: ROPE, created: BasicTime.GMT, bytes: INT] RETURNS [refused: BOOL _ FALSE]] ~ { matches: BOOL _ FALSE; first: BOOL _ TRUE; Info: PROC [fullFName, attachedTo: ROPE, created: BasicTime.GMT, bytes: INT, keep: CARDINAL] RETURNS [continue: BOOL _ TRUE] ~ { refused: BOOL; IF (first AND cs.mark = STPOps.markNewDirectory) OR cs.mark = STPOps.markDirectory THEN PupStream.SendMark[stream, STPOps.markHereIsPList]; refused _ action[stream: stream, userProps: userProps, fullFName: fullFName, created: created, bytes: bytes]; IF NOT refused THEN matches _ TRUE; first _ FALSE; }; Name: PROC [fullFName: ROPE] RETURNS [continue: BOOL _ TRUE] ~ { refused: BOOL; IF (first AND cs.mark = STPOps.markNewDirectory) OR cs.mark = STPOps.markDirectory THEN PupStream.SendMark[stream, STPOps.markHereIsPList]; refused _ action[stream: stream, userProps: userProps, fullFName: fullFName, created: BasicTime.nullGMT, bytes: 0]; IF NOT refused THEN matches _ TRUE; first _ FALSE; }; expl: ROPE _ NIL; group: FS.ErrorGroup; pattern: ROPE _ NIL; GetUserProperties[cs, userProps]; IF NOT ValidUser[userProps.userName, userProps.userPassword, cs.mark] THEN { No[stream, accessDenied, "Access denied"]; RETURN; }; IF userProps.version = NIL AND cs.mark = STPOps.markRetrieve THEN { userProps.version _ "H"; }; pattern _ Rope.Cat[ FullFNameFromUserProperties[userProps ! FS.Error => {expl _ error.explanation; group _ error.group; CONTINUE} ], IF userProps.version.Length > 0 THEN "!" ELSE NIL, userProps.version ]; IF expl # NIL THEN {GenerateFileError[stream, group, expl]; RETURN}; IF userProps.desiredProperty[createDate] OR userProps.desiredProperty[size] THEN { FS.EnumerateForInfo[pattern: pattern, proc: Info ! FS.Error => {expl _ error.explanation; group _ error.group; CONTINUE} ]; } ELSE { FS.EnumerateForNames[pattern: pattern, proc: Name ! FS.Error => {expl _ error.explanation; group _ error.group; CONTINUE} ]; }; IF expl # NIL THEN {GenerateFileError[stream, group, expl]; RETURN}; IF matches AND (cs.mark = STPOps.markNewDirectory OR cs.mark = STPOps.markDirectory) THEN Finish[stream, ""]; IF NOT matches THEN No[stream, fileNotFound, "File not found"]; }; RopeFromMark: PROC [mark: NAT] RETURNS [ROPE] ~ { RETURN [SELECT mark FROM STPOps.markRetrieve => "Retrieve", STPOps.markNewStore => "New-Store", STPOps.markComment => "Comment", STPOps.markIAmVersion => "Version", STPOps.markDirectory => "Enumerate", STPOps.markNewDirectory => "New-Enumerate", STPOps.markDelete => "Delete", STPOps.markYes => "Yes", STPOps.markNo => "No", ENDCASE => Convert.RopeFromInt[mark, 8] ] }; MakeNewServerViewer: PROC [stream: STREAM, pupAddress: PupTypes.PupAddress] ~ { otherGuy: ROPE _ PupDefs.GetHostName[pupAddress]; userProps: UserProperties _ NEW[UserPropertiesRep]; cs: CommandString; closing: BOOL _ FALSE; closeReason: PupStream.CloseReason _ localAbort; viewer: ViewerClasses.Viewer _ MakeMyKindOfTypescript[Rope.Concat["STPServer ", otherGuy]]; viewerOut: STREAM _ IF viewer = NIL THEN NIL ELSE ViewerIO.CreateViewerStreams[name: viewer.name, viewer: viewer, editedStream: FALSE].out; stream.propList _ Atom.PutPropOnList[stream.propList, $STPServerViewerStream, viewerOut]; { ENABLE { PupStream.StreamClosing => {closeReason _ why; closing _ TRUE; GOTO Exit}; PupStream.TimeOut => {closeReason _ transmissionTimeout; closing _ TRUE; GOTO Exit}; RuntimeError.UNCAUGHT => IF NOT debugging THEN {closeReason _ localAbort; GOTO Exit}; ABORTED => {closeReason _ localAbort; GOTO Exit}; }; AwaitCallingMessage[stream]; IF NOT closing THEN SendHerald[stream]; UNTIL closing OR (viewerOut # NIL AND ViewerIO.GetViewerFromStream[viewerOut].destroyed) DO ok: BOOL _ TRUE; cs _ GetCommandString[stream]; SELECT cs.mark FROM STPOps.markDirectory => DoFiles[stream, userProps, cs, SendPropList]; STPOps.markNewDirectory => DoFiles[stream, userProps, cs, SendPropList]; STPOps.markRetrieve => DoFiles[stream, userProps, cs, RetrieveFile]; STPOps.markStore => StoreFile[stream, userProps, cs, FALSE]; STPOps.markNewStore => StoreFile[stream, userProps, cs]; STPOps.markDelete => DoFiles[stream, userProps, cs, DeleteFile]; STPOps.markComment => NULL; ENDCASE => No[stream, STPReplyCode.ReplyCode.badCommand, "Command undefined or unimplemented"]; ENDLOOP; EXITS Exit => NULL; }; IO.Close[stream ! IO.Error => CONTINUE]; IF viewerOut # NIL THEN { IO.PutF1[viewerOut, "\nClosing %g\n", [refAny[NEW[PupStream.CloseReason _ closeReason]]] ! IO.Error => CONTINUE]; Process.Pause[Process.MsecToTicks[1500]]; IF viewer.iconic AND NOT debugging THEN ViewerOps.DestroyViewer[viewer]; }; }; CommandString: TYPE ~ RECORD [ mark: NAT _ 0, string: ROPE _ NIL ]; UserProperties: TYPE ~ REF UserPropertiesRep; UserPropertiesRep: TYPE ~ RECORD [ userName: ROPE _ NIL, userPassword: ROPE _ NIL, directory: ROPE _ NIL, nameBody: ROPE _ NIL, serverName: ROPE _ NIL, version: ROPE _ NIL, createdTime: ROPE _ NIL, size: ROPE _ NIL, desiredProperty: ARRAY STP.ValidProperties OF BOOL _ ALL[FALSE] ]; GetCommandString: PROC [stream: STREAM, markConsumed: BOOL _ FALSE, mark: NAT _ 0] RETURNS [cs: CommandString] ~ { isMark: BOOL _ FALSE; char: CHAR _ '?; closed: BOOL _ FALSE; text: REF TEXT _ RefText.ObtainScratch[100]; GetChar: PROC ~ { ok: BOOL _ TRUE; char _ '?; isMark _ FALSE; char _ stream.GetChar[ ! IO.Error => {closed _ TRUE; CONTINUE}; IO.EndOfStream => {isMark _ TRUE; CONTINUE}; ]; }; GetTextToMark: PROC ~ { text.length _ 0; GetChar[]; UNTIL isMark OR closed DO text _ RefText.AppendChar[text, char]; GetChar[]; ENDLOOP; }; cs.string _ NIL; cs.mark _ mark; IF NOT markConsumed THEN { cs.mark _ 0; GetTextToMark[]; IF text.length # 0 THEN SIGNAL ProtocolError; IF closed THEN RETURN; cs.mark _ PupStream.ConsumeMark[stream]; }; GetTextToMark[]; cs.string _ Rope.FromRefText[text]; RefText.ReleaseScratch[text]; { t: NAT _ PupStream.ConsumeMark[stream]; IF t#STPOps.markEOC THEN SIGNAL ProtocolError }; WITH Atom.GetPropFromList[stream.propList, $STPServerViewerStream] SELECT FROM viewerOut: STREAM => { IO.PutF[viewerOut, "\n%g %g\n", [rope[RopeFromMark[cs.mark]]], [rope[HidePassword[cs.string]]] ! IO.Error => CONTINUE]; }; ENDCASE; }; ProtocolError: SIGNAL ~ CODE; AwaitCallingMessage: PROC [stream: STREAM] ~ { cs: CommandString _ GetCommandString[stream]; WHILE cs.mark # STPOps.markIAmVersion DO SIGNAL ProtocolError; cs _ GetCommandString[stream]; ENDLOOP; }; SendHerald: PROC [stream: STREAM] ~ { PupStream.SendMark[stream, STPOps.markIAmVersion]; IO.PutChar[stream, VAL[1]]; Finish[stream, heraldMessage]; }; NarrowToList: PROC [lisp: REF] RETURNS [LORA _ NIL] ~ { WITH lisp SELECT FROM list: LORA => RETURN [list]; ENDCASE; }; NarrowToRope: PROC [lisp: REF] RETURNS [ROPE _ NIL] ~ { WITH lisp SELECT FROM rope: ROPE => RETURN [rope]; ENDCASE; }; propertyNames: ARRAY STP.ValidProperties OF ROPE ~ [ 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" ]; ValidPropertyFromRope: PROC [propName: ROPE] RETURNS [prop: STP.ValidProperties] ~ { FOR p: STP.ValidProperties IN STP.ValidProperties DO IF Rope.Equal[propName, propertyNames[p], FALSE] THEN RETURN [p]; ENDLOOP; SIGNAL ProtocolError; }; GetUserProperties: PROC [cs: CommandString, userProperties: UserProperties] ~ { stream: STREAM _ IO.RIS[cs.string]; lisp: REF; userProperties.directory _ NIL; userProperties.nameBody _ NIL; userProperties.serverName _ NIL; userProperties.version _ NIL; userProperties.size _ NIL; userProperties.createdTime _ NIL; lisp _ ParseLisp[stream ! IO.Error, IO.EndOfStream => {SIGNAL ProtocolError; CONTINUE}]; IF NOT stream.EndOf THEN SIGNAL ProtocolError; FOR p: LORA _ NarrowToList[lisp], p.rest UNTIL p = NIL DO q: LORA _ NarrowToList[p.first]; IF q = NIL OR q.rest = NIL THEN SIGNAL ProtocolError; WITH q.first SELECT FROM key: ROPE => { arg: ROPE _ NarrowToRope[q.rest.first]; IF key.Equal["Desired-property", FALSE] THEN { prop: STP.ValidProperties _ ValidPropertyFromRope[arg]; userProperties.desiredProperty[prop] _ TRUE; } ELSE { prop: STP.ValidProperties _ ValidPropertyFromRope[key]; SELECT prop FROM userName => userProperties.userName _ arg; userPassword => userProperties.userPassword _ arg; connectName => NULL; connectPassword => NULL; byteSize => NULL; type => NULL; size => userProperties.size _ arg; directory => userProperties.directory _ arg; nameBody => userProperties.nameBody _ arg; version => userProperties.version _ arg; createDate => { userProperties.createdTime _ arg; FOR t: LORA _ q.rest.rest, t.rest UNTIL t = NIL DO r: ROPE _ NarrowToRope[t.first]; userProperties.createdTime _ userProperties.createdTime.Cat[" ", r]; ENDLOOP; }; readDate => NULL; writeDate => NULL; author => NULL; eolConversion => NULL; account => NULL; userAccount => NULL; device => NULL; serverName => userProperties.serverName _ arg; ENDCASE => NULL; }; }; ENDCASE => SIGNAL ProtocolError; ENDLOOP; }; ParseLisp: PROC [stream: STREAM] RETURNS [ref: REF ANY _ NIL] ~ { char: CHAR _ IO.GetChar[stream]; WHILE char = ' DO char _ IO.GetChar[stream] ENDLOOP; IF char # '( THEN { text: REF TEXT _ RefText.ObtainScratch[100]; nextUpper: BOOL _ TRUE; text.length _ 0; WHILE char # ' AND char # '( AND char # ') DO IF char = '\' -- single quote -- THEN { char _ IO.GetChar[stream]; }; text _ RefText.AppendChar[text, char]; char _ IO.GetChar[stream]; ENDLOOP; ref _ Rope.FromRefText[text]; RefText.ReleaseScratch[text]; stream.Backup[char]; } ELSE { list: LORA _ NIL; char _ IO.GetChar[stream]; WHILE char = ' DO char _ IO.GetChar[stream] ENDLOOP; WHILE char # ') DO stream.Backup[char]; list _ CONS[ParseLisp[stream], list]; char _ IO.GetChar[stream]; ENDLOOP; ref _ List.Reverse[list]; }; }; CopyStream: PROC [from, to: STREAM, fileByteSize: INT] ~ { text: REF TEXT _ RefText.ObtainScratch[512]; viewer: ViewerClasses.Viewer _ FindMyViewer[from, to]; toGo: INT _ fileByteSize; WHILE IO.GetBlock[from, text, 0] > 0 DO IF viewer # NIL THEN SetPieFraction[viewer, REAL[toGo]/MAX[fileByteSize, 1]]; IO.PutBlock[to, text]; toGo _ toGo - text.length; ENDLOOP; IF viewer # NIL THEN SetPieFraction[viewer, REAL[toGo]/MAX[fileByteSize, 1]]; RefText.ReleaseScratch[text]; }; ftpListener: PupStream.PupListener _ NIL; STPServerCommand: Commander.CommandProc = { argv: CommandTool.ArgumentVector _ CommandTool.Parse[cmd: cmd, starExpand: FALSE ! CommandTool.Failed => {msg _ errorMsg; GO TO failed}]; out: STREAM = cmd.out; ShowOption: PROC [option: ROPE] = { out.PutF1[" STP Server %g.\n", [rope[option]]]; }; FOR i: NAT IN [1..argv.argc) DO arg: ROPE = argv[i]; SELECT TRUE FROM Rope.Equal[arg, "stop", FALSE] => { IF ftpListener # NIL THEN { PupStream.DestroyPupListener[ftpListener]; ftpListener _ NIL; }; StopSingle[]; ShowOption["stopped"]; }; Rope.Equal[arg, "start", FALSE], ftpListener = NIL => { ftpListener _ PupStream.CreatePupByteStreamListener[ local: PupTypes.ftpSoc, proc: MakeNewServerViewer, ticks: PupStream.SecondsToTocks[60] ]; IF useSingle THEN StartSingle[]; ShowOption["started"]; }; Rope.Equal[arg, "loose", FALSE] => { promiscuous _ TRUE; ShowOption["promiscuous"]; }; Rope.Equal[arg, "tight", FALSE] => { promiscuous _ FALSE; ShowOption["hard to get"]; }; Rope.Equal[arg, "mute", FALSE] => { mute _ TRUE; ShowOption["mute"]; }; Rope.Equal[arg, "noisy", FALSE] => { mute _ FALSE; ShowOption["noisy"]; }; Rope.Equal[arg, "single", FALSE] => { useSingle _ TRUE; ShowOption["single"]; }; Rope.Equal[arg, "noSingle", FALSE] => { useSingle _ FALSE; ShowOption["noSingle"]; }; ENDCASE => { ShowOption["running"]; }; ENDLOOP; EXITS failed => result _ $Failed; }; MakeMyKindOfTypescript: PROC [name: ROPE] RETURNS [viewer: ViewerClasses.Viewer _ NIL] ~ { IF NOT mute THEN { viewer _ TypeScript.Create[info: [name: name, iconic: TRUE], paint: FALSE]; ViewerOps.AddProp[viewer, $PieData, NEW[PieDataRep _ [iconNumber: maxIcon]]]; viewer.icon _ private; viewer.class _ fakeTypescriptClass; ViewerOps.PaintViewer[viewer, all, TRUE]; }; }; FindMyViewer: PROCEDURE [from, to: IO.STREAM] RETURNS [viewer: ViewerClasses.Viewer _ NIL] ~ { viewerStream: IO.STREAM _ NARROW[Atom.GetPropFromList[from.propList, $STPServerViewerStream]]; IF viewerStream = NIL THEN viewerStream _ NARROW[Atom.GetPropFromList[to.propList, $STPServerViewerStream]]; IF viewerStream # NIL THEN viewer _ ViewerIO.GetViewerFromStream[viewerStream]; }; PieData: TYPE ~ REF PieDataRep; PieDataRep: TYPE ~ RECORD [ iconNumber: INT ]; maxIcon: NAT _ 19; SetPieFraction: PROC [self: ViewerClasses.Viewer, fraction: REAL] ~ { iconNumber: INT _ Real.RoundLI[MAX[MIN[fraction, 1.0], 0.0]*maxIcon]; old: INT _ -1; IF iconNumber = 0 AND fraction>0 THEN iconNumber _ 1; WITH ViewerOps.FetchProp[self, $PieData] SELECT FROM pieData: PieData => { old _ pieData.iconNumber; pieData.iconNumber _ iconNumber; }; ENDCASE => NULL; IF iconNumber # old AND self.iconic THEN { ViewerOps.PaintViewer[self, all, FALSE]; }; }; MyPaint: ViewerClasses.PaintProc ~ { IF self.iconic THEN { iconNumber: INT _ maxIcon; WITH ViewerOps.FetchProp[self, $PieData] SELECT FROM pieData: PieData => { iconNumber _ pieData.iconNumber; }; ENDCASE => NULL; MyPaintIcon[context, iconNumber, self.name]; IF self = TiogaOps.GetSelection[primary].viewer THEN { [] _ Imager.SetColor[context, ImagerBackdoor.invert]; Imager.MaskBox[context, [0, 0, Icons.iconW, Icons.iconH]]; }; } ELSE [] _ typescriptPaint[self, context, whatChanged, clear]; }; iconStream: STREAM _ NIL; myIconRef: Icons.IconRef _ NEW[Icons.IconRep]; myIconFlavor: Icons.IconFlavor _ Icons.NewIcon[myIconRef]; MyPaintIcon: ENTRY PROC [context: Imager.Context, iconNumber: INT, name: ROPE] ~ TRUSTED { ENABLE {UNWIND => NULL; RuntimeError.UNCAUGHT => CONTINUE}; bytes: INT _ SIZE[Icons.IconFileFormat]*Basics.bytesPerWord; block: Icons.IconFileFormat; base: LONG POINTER _ @block; iconStream.SetIndex[iconNumber*bytes]; [] _ iconStream.UnsafeGetBlock[[base: base, startIndex: 0, count: bytes]]; myIconRef.bits _ block.bits; myIconRef.label _ block.label; myIconRef.invertLabel _ block.invertLabel; myIconRef.lx _ block.lx; myIconRef.ly _ block.ly; myIconRef.lw _ block.lw; myIconRef.lh _ block.lh; Icons.DrawIcon[myIconFlavor, context, 0, 0, name]; }; fakeTypescriptClass: ViewerClasses.ViewerClass _ NIL; typescriptPaint: ViewerClasses.PaintProc _ NIL; iconFileName: ROPE _ "STPServer.icons"; LookupFileType: PupTypes.PupType = LOOPHOLE[200B]; LookupFileReplyType: PupTypes.PupType = LOOPHOLE[201B]; LookupFileErrorType: PupTypes.PupType = LOOPHOLE[202B]; LookupFileSocket: PupTypes.PupSocketID = [0, 61B]; singleSocket: PupDefs.PupSocket _ NIL; singleProcess: CedarProcess.Process _ NIL; StartSingle: ENTRY PROC = { ENABLE UNWIND => NULL; IF singleProcess = NIL THEN { singleSocket _ PupDefs.PupSocketMake [local: LookupFileSocket, remote: PupTypes.fillInPupAddress, ticks: PupDefs.veryLongWait ]; singleProcess _ CedarProcess.Fork[ SinglePacketListener, NIL, [priority: foreground, usePriority: TRUE]]; }; }; StopSingle: ENTRY PROC = { ENABLE UNWIND => NULL; IF singleProcess # NIL THEN { CedarProcess.Abort[singleProcess]; PupDefs.PupSocketDestroy[singleSocket ! RuntimeError.UNCAUGHT => CONTINUE]; singleSocket _ NIL; }; }; ReplyRecord: TYPE = MACHINE DEPENDENT RECORD[v: CARDINAL, c, l: LongNumber]; ReplyRecordBytes: CARDINAL = SIZE[ReplyRecord]*Basics.bytesPerWord; SinglePacketListener: CedarProcess.ForkableProc = TRUSTED { ENABLE UNWIND => singleProcess _ NIL; text: REF TEXT _ NEW[TEXT[256]]; DO receiver: PupDefs.PupBuffer = singleSocket.get[]; replyType: PupTypes.PupType _ error; replyBytes: CARDINAL _ 0; IF receiver # NIL THEN { addr: PupTypes.PupAddress _ receiver.source; msWait: CARDINAL = 2000+500*MIN[8, PupDefs.GetHopsToNetwork[addr.net]]; IF receiver.pupType = LookupFileType THEN { id: PupTypes.Pair _ receiver.pupID; len: INT _ PupDefs.GetPupContentsBytes[receiver]; IF len IN [1..256] THEN { ENABLE RuntimeError.UNCAUGHT => GO TO dropIt; sLen: NAT _ len; name: ROPE _ NIL; fileLen: INT _ 0; fileDate: GMT _ BasicTime.nullGMT; rPtr: LONG POINTER TO ReplyRecord = LOOPHOLE[@receiver.pupBody]; FOR i: NAT IN [0 .. sLen) DO text[i] _ receiver.pupChars[i]; ENDLOOP; text.length _ sLen; name _ Rope.Flatten[Rope.Concat["[]", RefText.TrustTextAsRope[text]]]; [fullFName: name, created: fileDate, bytes: fileLen] _ FS.FileInfo[name: name ! FS.Error => { IF error.code = $unknownFile THEN { replyType _ LookupFileErrorType; }; CONTINUE }; ]; IF fileDate # BasicTime.nullGMT AND fileLen >= 0 THEN { rPtr.v _ CurrentVersion[name]; rPtr.c _ Basics.SwapHalves[[lc[BasicTime.ToPupTime[fileDate]]]]; rPtr.l _ Basics.SwapHalves[[li[fileLen]]]; replyType _ LookupFileReplyType; replyBytes _ ReplyRecordBytes; }; }; EXITS dropIt => {}; }; receiver.pupType _ replyType; IF replyType = error THEN receiver.errorCode _ noProcessPupErrorCode; PupDefs.ReturnPup[receiver, replyType, replyBytes]; }; CedarProcess.CheckAbort[singleProcess]; ENDLOOP; }; Init: PROC ~ { fakeTypescriptClass _ NEW[ViewerClasses.ViewerClassRec _ ViewerOps.FetchViewerClass[$Typescript]^]; typescriptPaint _ fakeTypescriptClass.paint; fakeTypescriptClass.paint _ MyPaint; iconStream _ FS.StreamOpen[ fileName: iconFileName, accessOptions: $read, streamOptions: [ tiogaRead: FALSE, commitAndReopenTransOnFlush: TRUE, truncatePagesOnClose: FALSE, finishTransOnClose: TRUE, closeFSOpenFileOnClose: TRUE ], streamBufferParms: [vmPagesPerBuffer: 2, nBuffers: 1] ]; Commander.Register["STPServer", STPServerCommand, "STPServer [ start | stop ]\n[]<>STPServer.readAccess should have list of permissible readers"]; }; Init[]; END. HSTPServerImpl.mesa Copyright (C) 1984, 1985 Xerox Corporation. All rights reserved. Michael Plass, August 22, 1985 3:50:50 pm PDT Jim Gasbarro, March 8, 1985 11:39:14 am PST Russ Atkinson (RRA) June 21, 1985 1:05:20 pm PDT *** Remember to update the date in heraldMessage when you make changes. supply default registry from the logged-in user. The currently logged-in user is welcome to do anything. Seconds for a cache entry to remain vaild The user is OK, and the last check is recent The user needs verification or something, so kill the cache We need a new access entry, and the user needs verification The server can be started up to be easy. Sets the given file name to the desired version (provided that it is not there already) may raise PupStream.StreamClosing, PupStream.TimeOut This hack required because the time may contain embedded spaces [cmd: Commander.Handle] RETURNS [result: REF ANY _ NIL, msg: ROPE _ NIL] Each argument can either be a switch specification or a genuine argument to be processed. The first argument (argv[0]) is not examined, because by convention it is the name of the command as given by the user. When an icon is selected it is inverted This bletch is because ViewerIO insists that viewer.class.flavor = $Typescript Single Packet Protocol RRA: There must be some better way to do this Initialization Ê.‡˜code™K™AK™-K™+K™0K™—šÏbG™GK™—šÏk ˜ Jšœžœ˜Jšœžœ"˜,Jšœžœ(˜4Jšœ žœžœB˜VJšœ žœ2˜DJšœ žœ˜(Jšœ žœ!˜2Jšœžœ#˜0Jšœžœ˜)JšžœžœÓ˜ÛJšœ žœ˜"Jšœ žœ˜#Jšœžœ$˜1JšœžœQ˜\Jšœžœ˜*Jšœžœ ˜Jš žœžœsžœžœ žœ˜´Jšœžœžœ ˜Jšœžœ˜#Jšœžœ†˜“Jšœ žœŒ˜›Jšœ žœD˜RJšœžœ ˜Jšœžœ>˜KJšœžœJžœ˜jJšœ žœ ˜Jšœ žœžœ˜Jšžœžœ˜Jšœžœ®˜ºJšœ žœ ˜Jšœžœ%˜1Jšœ žœ˜Jšœ žœ ˜Jšœžœ˜Jšœžœ2˜EJšœ žœ,˜:Jšœ žœD˜S—šÐln œžœž˜KšžœVžœBžœ”˜·šœž˜K˜—šœžœ8˜KK˜—Kšžœžœ žœ˜Kšœ žœ˜%Kš žœžœžœžœžœžœ˜Kšžœžœžœ˜Kšžœžœžœžœ˜K˜Kšœ žœžœ˜Kšœ žœžœ˜Kšœ žœžœ˜šœžœžœ˜K˜—šœžœžœ˜$Kšœ žœ˜Kšœ˜Kšœ˜K˜—šÏn œžœžœžœžœ žœžœ˜VKšžœžœžœ˜Kšœ>˜>Kšœžœžœ˜Kšœžœ˜Kšœžœ˜ Kšœ-˜-Kšœ!˜!Kšœ!˜!šžœžœžœ˜/Kšœ9˜9K™0Kšœ˜—š žœžœžœžœž˜KK™7Kšžœžœ˜—šžœž˜šœ˜Kšžœ-˜3—šœ(˜(Kšžœ/˜5—šœ0˜0Kšžœ%žœ˜1—šžœ˜ Kšžœ,˜2——Kšœ˜K˜—Kšœ-žœ˜4šœžœžœ˜$Kšœ)™)—K˜Kšœ žœžœ˜'šœžœžœ˜Kšœ!˜!Kšœžœ˜/Kšœ"˜"Kšœ$˜$Kšœ ˜ K˜K˜—šœ žœ˜'K˜—š   œžœžœ+žœžœžœ˜^šœ žœžœž˜$Kšœ$˜$Kšœ(˜(Kšœ"˜"Kšžœžœ˜—Kšœžœ˜Kšœžœ˜%šžœ%žœž˜4šœ˜šžœ&žœE˜pšžœ˜Kšœ,™,—šžœ'˜+K™;———Kšžœ˜—šžœžœžœ˜Kšœ;™;šœžœ˜$Kšœ˜Kšœ˜K˜—šžœ;ž˜AKšžœžœ˜—Kšœ2˜2K˜—šžœ žœžœžœ˜"Kšœ(™(—šžœž˜˜Kšœ˜šžœž˜Kšœ(˜(Kšœ,˜,Kšœ&˜&Kšžœ˜—šžœž˜Kšœžœžœ˜Kšœžœžœ˜Kšžœ˜—K˜—Kšžœžœžœ˜—šžœžœžœ˜Kš œ žœ žœ žœžœ˜DKšœžœ˜Kšœžœ˜Kšœ%žœ˜,šžœ ž˜Kšžœ žœ"˜4šžœ%ž˜+Kšœ*˜*—šžœžœžœžœ˜[Kšžœ-žœ˜7Kšžœžœžœ˜—Kšžœ˜—Kšœ!˜!šžœ ž˜Kšœ žœ˜%Kšœžœ˜šžœ žœ,ž˜AKšœ˜Kšžœ˜—šžœžœ/ž˜PKšœ˜Kšžœ˜—šžœIž˜OKšžœžœ˜ —Kšœ.˜.Kšžœ˜—Kšžœžœ˜ šž˜šœžœž˜Kšœ%˜%Kšœ)˜)Kšœ#˜#Kšžœ˜—šœžœž˜šœ˜Kšœ˜—šœ ˜ Kšœ8˜8—šœ˜KšœP˜P—Kšžœ˜—K˜—K˜—Kšžœžœ˜K˜K˜—š   œžœžœžœžœ˜.Kš žœžœžœžœžœžœ˜FKšœ˜K˜—š   œžœžœžœžœ˜2Kšœžœžœ˜*Kšœžœ˜ Kšžœžœ ˜Kšžœžœ˜%KšžœžœF˜SKšžœ˜ Kšœ˜K˜—šœ žœžœžœ_˜K˜—š  œžœ žœžœ˜;Kšœ6˜6Kšœžœ˜Kšžœržœ˜Kšžœr˜tšœžœž˜#K˜K˜K˜K˜K˜Kšžœ˜—šžœ žœ˜Kšžœ˜Kšžœ˜Kšžœžœžœžœ˜:Kšžœ˜Kšœ˜—Kšœ˜K˜—š  œžœ žœ(žœžœ žœžœ žœžœ˜—Kšœžœ˜Kš œžœžœžœžœ˜OKšœŽžœ˜“Kšœžœ˜+Kšžœ˜šžœžœ˜%Kšžœ'˜)šžœ˜Kšžœžœ9˜@Kšžœžœ@˜G—Kšžœ˜Kšžœ5˜7Kšžœ˜Kšœ˜—šžœžœ˜$Kšžœ ˜"šžœ˜Kšžœžœ9˜@Kšžœžœ@˜G—Kšžœžœžœ˜5KšžœH˜JKšžœ˜Kšœ˜—šžœžœ˜#Kšžœ ˜"Kšžœ\˜^Kšžœ˜Kšœ˜—šžœžœ˜"Kšžœ˜ Kšžœ@˜BKšžœ˜Kšœ˜—šžœžœ˜%Kšžœ$˜&Kšœ˜Kšžœ˜Kšœ˜—šžœžœ˜#Kšžœ"˜$Kšœ˜—šžœžœ˜Kšžœ"˜$Kšœ˜—šžœžœ˜Kšžœ.˜0Kšœ˜—Kšžœ˜Kšœ˜K˜—š  œžœ žœ(žœžœ žœžœ žœžœ˜—Kšœžœžœ˜Kšœžœ˜#Kšœ˜Kšœ:˜:Kšœ˜šžœ ž˜šœ˜šžœ˜Kšžœ:žœžœ˜IKšžœ.žœžœ˜=K˜—Kšœžœ˜š œžœ*žœžœžœžœžœ˜±Kšœ˜—Kšœ2˜2KšœC˜CKšžœ˜Kšœ!˜!Kšžœ0˜5Kšœ˜—Kšœžœ˜ Kšžœ˜—Kšœ˜K˜—š  œžœ žœ(žœžœ žœžœ žœžœ˜•Kšœžœžœ˜Kšœžœ˜Kšœ˜Kšœ:˜:Kšœ˜šžœ ž˜šœ˜šžœ˜Kšœžœ:žœžœ˜JKšœ˜—Kšœ˜Kšžœ1˜6K˜—Kšœžœ˜ Kšžœ˜—Kšœ˜K˜—š œžœžœ žœ˜[šžœž˜šžœ˜Kšœžœ˜#Kšžœžœžœ"˜RKšœ žœG˜UKšœ˜—šžœ˜Kšœžœžœžœ˜Kšœžœ˜'Kšœ˜Kšžœ žœ#žœ˜DKšœ˜Kšžœ žœ(žœ˜IKšœ˜Kšœ˜Kšœ˜——Kšœ˜K˜—š   œžœ žœ:žœžœ˜iKšœžœžœ˜Kšœžœžœ˜Kšœžœ˜#Kšœžœ˜Kšœ žœžœ˜Kšœžœ˜Kšœžœ˜'Kšœ!˜!šžœžœ@žœ˜LKšœ*˜*Kšžœ˜Kšœ˜—˜šœ1˜1Kšœžœ:žœžœ ˜L—KšœHžœ˜RKšœ4˜4šžœžœž˜#šœ?˜?Kšœžœžœ˜%——šžœ žœžœ˜šœžœ ˜Kšœ˜Kšœ˜Kš œžœžœžœžœžœ˜šKšœ˜Kšœ ˜ Kšœžœ:žœžœ˜JKšœ˜—Kšœ˜—šžœ ˜ šžœ˜Kšœ žœžœ˜Kšœ žœ˜Kšœ žœ žœ˜5Kšœ žœ˜*Kšœžœ˜+Kšžœžœ˜0Kšžœ#žœ˜FKšœH˜HKšœ˜—KšžœF˜J—šžœ)ž˜3šœ˜Kšœ4žœ˜IKšœ˜—šœ˜šœA˜AKšœžœ.žœžœ˜?Kšœ˜—Kšœ˜šžœ ž˜šœ˜Kšœ žœ žœ˜5Kšœžœžœ˜,šœ ˜ šœžœ ˜Kšœ#žœžœ˜/—Kšœ˜—šžœ#žœžœžœ˜:šžœB˜DKšœžœ:žœžœ˜JKšœ˜—Kšœ˜—šžœžœžœ˜"Kšœžœ:žœžœ˜JKšœ˜—Kšžœžœžœ*žœ˜DKšœ"˜"šžœž˜šžœ!˜#Kšžœžœ7˜>Kšžœžœb˜i——K˜—šœ˜Kšœ žœ žœ˜5Kšœžœžœ˜,Kšœžœžœ˜6šœž˜Kšœžœ.žœžœ ˜@—Kšžœžœ žœ˜'Kšœ0˜0Kš žœžœžœžœžœ˜K˜—Kšžœžœ˜ —Kšœ˜—Kšžœžœ˜ —Kšžœ3žœ˜@K˜—Kšœ˜K˜—š  œžœžœ žœžœžœžœ˜JKšœW™Wšžœžœžœ˜.Kšœ žœ4˜EKšœ žœ˜šž˜šžœ&˜(Kšœžœ žœ˜Kšœ˜—šœ žœ*ž˜;Kšœžœ žœ˜Kšœ ˜ —Kšžœ$žœžœžœ˜9Kšžœ˜—Kšœ˜—K˜K˜—š  œžœžœžœ žœ ˜@Kšœžœ˜Kšœžœ˜%Kšžœ žœ%˜6K˜K˜—š   œžœžœžœžœžœ ˜BKšœžœ˜šžœžœžœ ž˜Kšœžœ˜Kš žœžœ žœžœžœ˜4Kš žœžœžœžœžœ˜&Kšžœ˜—K˜K˜—š  œžœ žœ žœžœ˜NKšœ žœžœ žœ˜DKšœ˜K˜—š œžœ žœ+žœ˜Lšžœ?žœž˜Nšœ žœ˜šœ)žœ˜GKšœžœ žœ˜Kšœ˜—Kšžœ˜——Kšœ*˜*Kšžœžœ ˜(Kšœ˜Kšœ˜K˜—š œžœ žœžœ˜*Kšœ+˜+Kšžœ˜Kšœ˜Kšœ˜K˜—š  œžœ žœžœžœ˜3Kšžœžœžœžœ˜,Kšœ+˜+Kšœ˜K˜—š   œžœ žœ(žœžœ žœ˜vKšœ3˜3Kšœ@˜@Kšœ˜Kšœ˜K˜—š œžœ žœ8žœ žœ(žœžœ žœžœ žœžœ˜ßKšœ žœžœ˜Kšœžœžœ˜š œžœžœžœ žœžœžœ žœžœ˜€Kšœ žœ˜Kšžœžœ$žœ˜RKšžœ4˜8Kšœm˜mKšžœžœ žœ žœ˜#Kšœžœ˜Kšœ˜—š  œžœ žœžœ žœžœ˜@Kšœ žœ˜Kšžœžœ$žœ˜RKšžœ4˜8Kšœs˜sKšžœžœ žœ žœ˜#Kšœžœ˜Kšœ˜—Kšœžœžœ˜Kšœžœ ˜Kšœ žœžœ˜Kšœ!˜!šžœžœ@žœ˜LKšœ*˜*Kšžœ˜Kšœ˜—šžœžœžœžœ˜CKšœ˜Kšœ˜—šœ˜šœ'˜'Kšžœ:žœ˜EKšœ˜—Kšžœžœžœžœ˜2Kšœ˜Kšœ˜—Kšžœžœžœ*žœ˜Dšžœ'žœ ˜Kšžœ˜šžœ.˜0Kšœžœ:žœ˜GK˜—Kšœ˜—šžœ˜šžœ/˜1Kšœžœ:žœ˜GK˜—Kšœ˜——Kšžœžœžœ*žœ˜Dšžœ žœ$žœ ˜TKšžœ˜—Kšžœžœ žœ,˜?Kšœ˜K˜—š   œžœžœžœžœ˜1šžœžœž˜K˜"Kšœ#˜#K˜ K˜#Kšœ$˜$Kšœ+˜+Kšœ˜Kšœ˜Kšœ˜Kšžœ ˜'Kšœ˜—Kšœ˜K˜—š œžœ žœ&˜OKšœ žœ#˜1Kšœžœ˜3K˜Kšœ žœžœ˜Kšœ0˜0Kšœ[˜[Kšœ žœžœ žœžœžœžœOžœ˜‹KšœY˜Yšœ˜šžœ˜Kšœ9žœžœ˜JKšœCžœžœ˜TKš œ žœžœžœ žœžœ˜UKšžœžœ˜1K˜—Kšœ˜Kšžœžœ žœ˜'š žœ žœžœžœ4ž˜[Kšœžœžœ˜Kšœ˜šžœ ž˜KšœE˜EKšœH˜HKšœD˜DKšœ5žœ˜˜^Jšžœžœžœžœ<˜lJšžœžœžœ5˜OKšœ˜K˜—Kšœ žœžœ ˜šœ žœžœ˜Kšœ ž˜Kšœ˜K˜—Kšœ žœ˜K˜š œžœ(žœ˜EKšœ žœžœžœ˜EKšœžœ˜Kšžœžœ žœ˜5šžœ%žœž˜4šœ˜Kšœ˜Kšœ ˜ Kšœ˜—Kšžœžœ˜—šžœžœ žœ˜*Kšœ!žœ˜(Kšœ˜—Kšœ˜K˜—šœ$˜$šžœ žœ˜Kšœ žœ ˜šžœ%žœž˜4šœ˜Kšœ ˜ Kšœ˜—Kšžœžœ˜—Kšœ,˜,šžœ.žœ˜6Kšœ'™'Kšœ5˜5Kšœ:˜:Kšœ˜—Kšœ˜—Kšžœ9˜=Kšœ˜K˜—šœ žœžœ˜K˜—Kšœžœ˜.šœ:˜:K˜—š   œžœžœ'žœžœžœ˜ZKš žœžœžœžœžœ˜;Kšœžœžœ+˜