DIRECTORY Ascii USING [Lower], Basics USING [Comparison], BasicTime, BTree, BTreeVM, Commander USING [CommandProc, Handle, Register], CommandTool USING [ArgumentVector, Failed, Parse], FS, FSBackdoor, GVBasics USING [MakeKey, Password, Timestamp], GVNames, GVSend, IO, Menus, PrincOpsUtils, Rope, TEditOps, UserCredentials, ViewerClasses, ViewerEvents, ViewerOps; PasswordAdmin: CEDAR MONITOR IMPORTS Ascii, BasicTime, BTree, BTreeVM, Commander, CommandTool, FS, FSBackdoor, GVBasics, GVNames, GVSend, IO, Menus, PrincOpsUtils, Rope, TEditOps, UserCredentials, ViewerEvents, ViewerOps SHARES Rope = BEGIN ROPE: TYPE = Rope.ROPE; RememberPasswordsCmd: Commander.CommandProc = BEGIN argv: CommandTool.ArgumentVector = CommandTool.Parse[cmd ! CommandTool.Failed => {cmd.out.PutRope[errorMsg]; GOTO stop}]; append: BOOLEAN _ FALSE; fileName: ROPE _ NIL; patterns: LIST OF ROPE _ NIL; FOR arg: NAT IN [1..argv.argc) DO SELECT TRUE FROM argv[arg].Length=0 => {cmd.out.PutRope["Syntax error"]; GOTO stop}; argv[arg].Fetch[0]='- => IF argv[arg].Length=2 AND Ascii.Lower[argv[arg].Fetch[1]]='a THEN append _ TRUE ELSE {cmd.out.PutRope["Illegal switch"]; GOTO stop}; fileName=NIL => fileName _ argv[arg]; ENDCASE => IF patterns=NIL THEN patterns _ LIST[argv[arg]] ELSE FOR item: LIST OF ROPE _ patterns, item.rest DO IF item.rest=NIL THEN {item.rest _ LIST[argv[arg]]; EXIT}; ENDLOOP; ENDLOOP; IF fileName=NIL OR patterns=NIL THEN {cmd.out.PutRope["Syntax error"]; GOTO stop}; IF ~GetWizardPassword[cmd] THEN GOTO stop; OpenBTree[fileName: fileName, create: NOT append ! FS.Error => {cmd.out.PutRope[error.explanation]; GOTO stop}]; FOR item: LIST OF ROPE _ patterns, item.rest UNTIL item=NIL DO cmd.out.PutF["Remember[\"%g\"]\n", [rope[item.first]]]; Remember[pattern: item.first, out: cmd.out]; ENDLOOP; CloseBTree[]; EXITS stop => cmd.out.PutChar['\n]; END; CheckPasswordsCmd: Commander.CommandProc = BEGIN argv: CommandTool.ArgumentVector = CommandTool.Parse[cmd ! CommandTool.Failed => {cmd.out.PutRope[errorMsg]; GOTO stop}]; append: BOOLEAN _ FALSE; fileName: ROPE _ NIL; patterns: LIST OF ROPE _ NIL; FOR arg: NAT IN [1..argv.argc) DO SELECT TRUE FROM argv[arg].Length=0 => {cmd.out.PutRope["Syntax error"]; GOTO stop}; argv[arg].Fetch[0]='- => IF argv[arg].Length=2 THEN {cmd.out.PutRope["Illegal switch"]; GOTO stop}; fileName=NIL => fileName _ argv[arg]; ENDCASE => IF patterns=NIL THEN patterns _ LIST[argv[arg]] ELSE FOR item: LIST OF ROPE _ patterns, item.rest DO IF item.rest=NIL THEN {item.rest _ LIST[argv[arg]]; EXIT}; ENDLOOP; ENDLOOP; IF fileName=NIL OR patterns=NIL THEN {cmd.out.PutRope["Syntax error"]; GOTO stop}; IF ~GetWizardPassword[cmd] THEN GOTO stop; OpenBTree[fileName: fileName, lock: $read ! FS.Error => {cmd.out.PutRope[error.explanation]; GOTO stop}]; FOR item: LIST OF ROPE _ patterns, item.rest UNTIL item=NIL DO invalid, unchanged, tooShort: LIST OF ROPE; cmd.out.PutF["Check[\"%g\"]\n", [rope[item.first]]]; [invalid: invalid, unchanged:unchanged, tooShort: tooShort] _ Check[pattern: item.first, out: cmd.out]; invalidNames _ ConcRopeLists[invalidNames, invalid]; unchangedNames _ ConcRopeLists[unchangedNames, unchanged]; tooShortNames _ ConcRopeLists[tooShortNames, tooShort]; ENDLOOP; CloseBTree[]; EXITS stop => cmd.out.PutChar['\n]; END; invalidNames, unchangedNames, tooShortNames: LIST OF ROPE _ NIL; GetWizardPassword: PROC [cmd: Commander.Handle] RETURNS [ok: BOOLEAN] = BEGIN IF wizardPassword=NIL THEN BEGIN password: ROPE; authenticateInfo: GVNames.AuthenticateInfo; cmd.out.PutRope["Enter wizard's password: "]; [token: password] _ cmd.in.GetTokenRope[]; cmd.out.PutRope[" ... "]; authenticateInfo _ GVNames.Authenticate[wizardName, password]; IF authenticateInfo#individual THEN { cmd.out.PutRope[Code[authenticateInfo]]; RETURN [FALSE]}; cmd.out.PutRope["ok\n"]; wizardPassword _ password; END; RETURN [TRUE]; END; wizardName: ROPE _ "Wizard.GV"; wizardPassword: ROPE _ NIL; Remember: PROC [pattern: Rope.ROPE, out: IO.STREAM] = BEGIN RememberWork: EntryEnumProc --[info: REF GVNames.GetEntryInfo] RETURNS [exit: BOOL _ FALSE]-- = BEGIN WITH info^ SELECT FROM i: GVNames.GetEntryInfo.individual => Insert[name: i.name, password: i.password, stamp: i.passwordStamp]; ENDCASE => out.PutF["Name \"%g\" is not an individual.\n", [rope[info.name]]]; END; IF tree=NIL OR tree.GetState[].state#open THEN {out.PutRope["Tree not open yet; call OpenBTree first.\n"]; RETURN}; EnumAllEntries[pattern: pattern, which: individuals, work: RememberWork, out: out]; END; Check: PROC [pattern: Rope.ROPE, out: IO.STREAM] RETURNS [invalid, unchanged, tooShort: LIST OF ROPE] = BEGIN CheckWork: EntryEnumProc --[info: REF GVNames.GetEntryInfo] RETURNS [exit: BOOL _ FALSE]-- = BEGIN WITH info^ SELECT FROM i: GVNames.GetEntryInfo.individual => BEGIN found, changed: BOOLEAN; password: GVBasics.Password; stamp: GVBasics.Timestamp; [found: found, password: password, stamp: stamp] _ Find[i.name]; IF ~found THEN { out.PutF["Name \"%g\" did not previously exist.\n", [rope[info.name]]]; RETURN}; changed _ password#i.password; SELECT KosherPassword[i.password] FROM $ok => IF ~changed THEN unchanged _ CONS[info.name, unchanged]; $invalid => invalid _ CONS[info.name, invalid]; $tooShort => IF changed THEN tooShort _ CONS[info.name, tooShort] ELSE unchanged _ CONS[info.name, unchanged]; ENDCASE; END; ENDCASE => out.PutF["Name \"%g\" is not an individual.\n", [rope[info.name]]]; END; invalid _ unchanged _ tooShort _ NIL; IF tree=NIL OR tree.GetState[].state#open THEN {out.PutRope["Tree not open yet; call OpenBTree first.\n"]; RETURN}; EnumAllEntries[pattern: pattern, which: individuals, work: CheckWork, out: out]; END; KosherPassword: PROC [password: GVBasics.Password] RETURNS [status: {ok, invalid, tooShort}] = BEGIN p: PasswordRep = LOOPHOLE[password]; FOR i: CARDINAL DECREASING IN [0..7] DO IF p[i].char#0 THEN RETURN [IF i<5 THEN $tooShort ELSE $ok]; REPEAT FINISHED => RETURN [$invalid]; ENDLOOP; END; RopeFromPassword: PROC [password: GVBasics.Password] RETURNS [ROPE] = BEGIN p: PasswordRep = LOOPHOLE[password]; FOR i: CARDINAL DECREASING IN [0..7] DO IF p[i].char#0 THEN BEGIN s: IO.STREAM _ IO.ROS[]; FOR j: CARDINAL IN [0..i] DO s.PutChar[LOOPHOLE[p[j].char]]; ENDLOOP; RETURN [IO.RopeFromROS[s]]; END; REPEAT FINISHED => RETURN [NIL]; ENDLOOP; END; PasswordRep: TYPE = PACKED ARRAY [0..7] OF RECORD [char: [0..177B], parity: BOOL]; ReadNamesFromTree: PROC RETURNS [names: LIST OF ROPE] = BEGIN Proc: BTreeEnumProc --[name: ROPE, password: GVBasics.Password, stamp: GVBasics.Timestamp] RETURNS [continue: BOOLEAN _ TRUE]-- = BEGIN IF KosherPassword[password]#invalid THEN names _ CONS[name, names]; END; names _ NIL; [] _ BTreeEnumerate[proc: Proc]; END; ConcRopeLists: PROC [a, b: LIST OF ROPE] RETURNS [LIST OF ROPE] = BEGIN IF a=NIL THEN RETURN[b]; IF b#NIL THEN FOR pred: LIST OF ROPE _ a, pred.rest DO IF pred.rest=NIL THEN {pred.rest _ b; EXIT}; ENDLOOP; RETURN[a]; END; BreakName: PROC [name: Rope.ROPE] RETURNS [sn, reg: Rope.ROPE] = BEGIN FOR i: INT DECREASING IN [0..name.Length[]) DO IF name.Fetch[i] = '. THEN RETURN[ sn: name.Substr[start: 0, len: i], reg: name.Substr[start: i+1, len: name.Length[]-(i+1)] ]; ENDLOOP; RETURN[ sn: NIL, reg: name ] END; Code: PROC [type: GVNames.Outcome] RETURNS [Rope.ROPE] = { RETURN[ SELECT type FROM noChange => "no change", group => "that's a group", individual => "that's an individual", notFound => "name not found", protocolError => "protocol error", wrongServer => "wrong server", allDown => "all suitable servers are down or inaccessible", badPwd => "incorrect password", outOfDate => "out of date", notAllowed => "you're not authorized to do that", ENDCASE => "unknown return code" ] }; NameClass: TYPE = { individuals, groups, dead }; NameEnumProc: TYPE = PROC [name: Rope.ROPE] RETURNS[exit: BOOL _ FALSE]; EnumAllNames: PROC [pattern: Rope.ROPE, which: NameClass, work: NameEnumProc, out: IO.STREAM] = BEGIN sn, reg: Rope.ROPE; [sn, reg] _ BreakName[pattern]; SELECT TRUE FROM reg.Find["*"]>=0 => BEGIN RegWork: NameEnumProc = { EnumAllNames[Rope.Cat[sn, ".", BreakName[name].sn], which, work, out]}; out.PutRope["Finding the registries . . .\n"]; EnumAllNames[Rope.Cat[reg, ".GV"], groups, RegWork, out]; END; sn.Find["*"]>=0 => TRUSTED BEGIN -- allegedly unsafe variant record assignments in here enumName: Rope.ROPE; info: GVNames.MemberInfo _ [noChange[]]; out.PutF["Enumerating names in %g . . .\n", [rope[reg]] ]; enumName _ Rope.Cat[ SELECT which FROM individuals => "Individuals.", groups => "Groups.", dead => "Dead.", ENDCASE => ERROR, reg]; info _ GVNames.GetMembers[enumName]; WITH info SELECT FROM i: GVNames.MemberInfo.group => BEGIN FOR m: GVNames.RListHandle _ i.members, m.rest UNTIL m = NIL DO IF Rope.Match[pattern: pattern, object: m.first, case: FALSE] THEN { IF work[m.first] THEN EXIT }; ENDLOOP; END; ENDCASE => out.PutF["Couldn't get members of \"%g\": %g\n", [rope[enumName]], [rope[Code[info.type]]]]; END; ENDCASE => [] _ work[pattern]; END; EntryEnumProc: TYPE = PROC [info: REF GVNames.GetEntryInfo] RETURNS [exit: BOOL _ FALSE]; EnumAllEntries: PROC [pattern: Rope.ROPE, which: NameClass, work: EntryEnumProc, out: IO.STREAM] = BEGIN EntryWork: NameEnumProc = BEGIN info: REF GVNames.GetEntryInfo; rc: GVNames.NameType; [rc, info] _ GVNames.AuthenticatedGetEntry[user: wizardName, password: pwd, name: name]; IF rc IN [group..individual] THEN exit _ work[info] ELSE out.PutF["Couldn't read entry for \"%g\": %g\n", [rope[name]], [rope[Code[rc]]]]; END; pwd: GVBasics.Password = GVBasics.MakeKey[wizardPassword]; EnumAllNames[pattern: pattern, which: which, work: EntryWork, out: out]; END; GetMessageText: PROC RETURNS [message: ROPE] = BEGIN initialForm: ROPE _ "Subject: \001subject\002\nTo: distribution:;\n\n\001message\002\n"; dateAndFrom: ROPE; RemoveMenuItem: PROC [name: ROPE] = BEGIN entry: Menus.MenuEntry = Menus.FindEntry[menu: viewer.menu, entryName: name]; IF entry#NIL THEN Menus.ReplaceMenuEntry[menu: viewer.menu, oldEntry: entry, newEntry: NIL]; END; AwaitComposedMessage: ENTRY PROC RETURNS [ok: BOOLEAN] = BEGIN action _ wait; WHILE action=wait DO WAIT messageComposed; ENDLOOP; RETURN [action=send]; END; IF viewer#NIL THEN {ViewerOps.DestroyViewer[viewer]; viewer _ NIL}; viewer _ ViewerOps.CreateViewer[flavor: $Text, info: [name: "PasswordAdmin composed message", iconic: FALSE]]; RemoveMenuItem["Reset"]; RemoveMenuItem["Get"]; RemoveMenuItem["GetImpl"]; RemoveMenuItem["PrevFile"]; RemoveMenuItem["Store"]; RemoveMenuItem["Save"]; Menus.AppendMenuEntry[menu: viewer.menu, entry: Menus.CreateEntry[name: "Send", proc: SendButton, fork: FALSE]]; Menus.AppendMenuEntry[menu: viewer.menu, entry: Menus.CreateEntry[name: "Abort", proc: AbortButton, fork: FALSE]]; ViewerOps.PaintViewer[viewer: viewer, hint: menu]; TEditOps.SetTextContents[viewer, initialForm]; [] _ ViewerEvents.RegisterEventProc[proc: DestroyButton, event: destroy]; IF ~AwaitComposedMessage[] THEN RETURN [NIL]; dateAndFrom _ IO.PutFR["Date: %g\nFrom: %g\n", , [rope[ArpaDate[BasicTime.Now[]]]], [rope[UserCredentials.Get[].name]]]; RETURN [Rope.Cat[dateAndFrom, TEditOps.GetTextContents[viewer]]]; END; SendMessage: PROC [recipients: LIST OF ROPE, message: ROPE, out: IO.STREAM] = BEGIN handle: GVSend.Handle = GVSend.Create[]; BEGIN ENABLE GVSend.SendFailed => { out.PutF["%g ... Retrying...\n", [rope[why]]]; RETRY}; startSendInfo: GVSend.StartSendInfo = handle.StartSend[senderPwd: UserCredentials.Get[].password, sender: UserCredentials.Get[].name, validate: FALSE]; IF startSendInfo#ok THEN {out.PutRope["StartSend failed unexpectedly\n"]; RETURN}; out.PutF["Sending recipient list... \n"]; FOR item: LIST OF ROPE _ recipients, item.rest UNTIL item=NIL DO handle.AddRecipient[item.first]; ENDLOOP; out.PutF["Sending message... "]; handle.StartText[]; handle.AddToItem[message]; handle.Send[]; out.PutF["Sent OK.\n"]; END; END; viewer: ViewerClasses.Viewer _ NIL; action: {wait, abort, send}; messageComposed: CONDITION; SendButton: ENTRY Menus.MenuProc = { action _ send; NOTIFY messageComposed}; AbortButton: ENTRY Menus.MenuProc = { action _ abort; NOTIFY messageComposed}; DestroyButton: ENTRY ViewerEvents.EventProc = { viewer _ NIL; action _ abort; NOTIFY messageComposed}; ArpaDate: PROC [gmt: BasicTime.GMT] RETURNS [text: ROPE] = BEGIN unpacked: BasicTime.Unpacked = BasicTime.Unpack[gmt]; zoneHour: CARDINAL = ABS[unpacked.zone]/60; zoneMinute: CARDINAL = ABS[unpacked.zone] MOD 60; zoneLetters: ARRAY [4..11] OF CHAR = ['A, 'E, 'C, 'M, 'P, 'Y, 'H, 'B]; dstLetters: ARRAY [0..2] OF CHAR = ['D, 'S, '?]; monthNames: ARRAY BasicTime.MonthOfYear OF ROPE = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "???"]; zoneText: ROPE = SELECT TRUE FROM unpacked.zone=721 => NIL, unpacked.zone>0 AND zoneHour IN [4..11] AND zoneMinute=0 => IO.PutFR["%g%gT", [character[zoneLetters[zoneHour]]], [character[dstLetters[LOOPHOLE[unpacked.dst]]]]], ENDCASE => IO.PutFR["%g%g:%02g", [character[IF unpacked.zone<0 THEN '- ELSE '+]], [cardinal[zoneHour]], [cardinal[zoneMinute]]]; date: ROPE = IO.PutFR["%2g %g %2g ", [cardinal[unpacked.day]], [rope[monthNames[unpacked.month]]], [cardinal[unpacked.year MOD 100]]]; time: ROPE = IO.PutFR["%2g:%02g:%02g %g", [cardinal[unpacked.hour]], [cardinal[unpacked.minute]], [cardinal[unpacked.second]], [rope[zoneText]]]; RETURN[Rope.Cat[date, time]]; END; tree: BTree.Tree _ NIL; file: FS.OpenFile _ FS.nullOpenFile; OpenBTree: PROC [fileName: ROPE, create: BOOL _ FALSE, lock: FS.Lock _ $write] = BEGIN vmHandle: BTreeVM.Handle; IF create AND lock#$write THEN ERROR; file _ IF create THEN FS.Create[name: fileName, pages: 40] ELSE FS.Open[name: fileName, lock: lock]; vmHandle _ BTreeVM.Open[file: FSBackdoor.GetFileHandle[file], filePagesPerPage: 4, cacheSize: 25]; IF tree=NIL THEN tree _ BTree.New[repPrim: [compare: BTreeCompare, compareEntries: NIL, entrySize: BTreeEntrySize, entryFromRecord: BTreeEntryFromRecord, newRecordFromEntry: BTreeNewRecordFromEntry], storPrim: [referencePage: BTreeVM.ReferencePage, releasePage: BTreeVM.ReleasePage], minEntrySize: BTreeEntry[0].SIZE]; BTree.Open[tree: tree, storage: vmHandle, pageSize: FS.WordsForPages[4], initialize: create]; END; CloseBTree: PROC = BEGIN IF tree#NIL THEN tree.SetState[closed]; IF file#FS.nullOpenFile THEN BEGIN pages, bytes: INT; [pages: pages, bytes: bytes] _ file.GetInfo[]; IF bytes#FS.BytesForPages[pages] THEN file.SetByteCountAndCreatedTime[bytes: FS.BytesForPages[pages]]; file.Close[]; END; file _ FS.nullOpenFile; END; Find: PROC [name: ROPE] RETURNS [found: BOOLEAN, password: GVBasics.Password, stamp: GVBasics.Timestamp] = TRUSTED BEGIN textName: Rope.Text = name.Flatten[]; entry: REF BTreeEntry = NARROW[tree.ReadRecord[key: textName]]; IF (found _ entry#NIL) THEN {password _ entry.password; stamp _ entry.stamp}; END; Insert: PROC [name: ROPE, password: GVBasics.Password, stamp: GVBasics.Timestamp] = TRUSTED BEGIN textName: Rope.Text = name.Flatten[]; entry: REF BTreeEntry = NEW[BTreeEntry[textName.Length[]] _ [password: password, stamp: stamp, name: ]]; PrincOpsUtils.LongCopy[to: BASE[DESCRIPTOR[entry^]], from: BASE[DESCRIPTOR[textName^]], nwords: (textName.Length[]+1)/2]; tree.UpdateRecord[key: textName, record: entry]; END; BTreeEnumerate: PROC [startingKey: ROPE _ NIL, relation: BTree.Relation _ equal, proc: BTreeEnumProc] RETURNS [exhausted: BOOLEAN] = TRUSTED BEGIN EnumEntry: UNSAFE PROC [entry: BTree.Entry] RETURNS [continue: BOOLEAN] = BEGIN e: LONG POINTER TO BTreeEntry = NARROW[entry]; t: Rope.Text = Rope.NewText[e.length]; PrincOpsUtils.LongCopy[to: BASE[DESCRIPTOR[t.text]], from: BASE[DESCRIPTOR[e.name]], nwords: (e.length+1)/2]; continue _ proc[name: t, password: e.password, stamp: e.stamp]; END; textName: Rope.Text = startingKey.Flatten[]; exhausted _ tree.EnumerateEntries[key: textName, relation: relation, Proc: EnumEntry]; END; BTreeEnumProc: TYPE = PROC [name: ROPE, password: GVBasics.Password, stamp: GVBasics.Timestamp] RETURNS [continue: BOOLEAN _ TRUE]; BTreeCompare: BTree.Compare --[key: Key, entry: Entry] RETURNS [Comparison]-- = UNCHECKED BEGIN k: REF TEXT = LOOPHOLE[NARROW[key, Rope.Text]]; e: LONG POINTER TO BTreeEntry = LOOPHOLE[entry]; FOR i: CARDINAL IN [0 .. MIN[k.length, e.length]) DO kc: CHAR _ Ascii.Lower[k[i]]; ec: CHAR _ Ascii.Lower[e[i]]; IF kcec THEN RETURN [greater]; ENDLOOP; IF k.lengthe.length THEN RETURN [greater]; RETURN [equal]; END; BTreeEntrySize: BTree.EntrySize --[entry: Entry] RETURNS [words: EntSize]-- = UNCHECKED BEGIN RETURN [BTreeEntry[LOOPHOLE[entry, LONG POINTER TO BTreeEntry].length].SIZE]; END; BTreeEntryFromRecord: BTree.EntryFromRecord --[record: Record] RETURNS [entry: Entry]-- = UNCHECKED { RETURN [LOOPHOLE [record]] }; BTreeNewRecordFromEntry: BTree.NewRecordFromEntry --[entry: Entry] RETURNS [record: Record]-- = UNCHECKED BEGIN e: LONG POINTER TO BTreeEntry = LOOPHOLE[entry]; record _ NEW [BTreeEntry[e.length]]; PrincOpsUtils.LongCopy[to: LOOPHOLE[record], from: e, nwords: BTreeEntry[e.length].SIZE]; END; BTreeEntry: TYPE = MACHINE DEPENDENT RECORD [ password: GVBasics.Password, stamp: GVBasics.Timestamp, -- most recent password update name: PACKED SEQUENCE length: CARDINAL OF CHAR]; Commander.Register[key: "RememberPasswords", proc: RememberPasswordsCmd, doc: "[-a] filename pattern1 pattern2 ..."]; Commander.Register[key: "CheckPasswords", proc: CheckPasswordsCmd, doc: "filename pattern1 pattern2 ..."]; END. HPasswordAdmin.mesa Tool for Grapevine password administration. Present functions are: read and save all passwords; compare passwords with ones previously saved and make a list of ones that haven't changed. Implementation adapted from DBPurge.mesa (Andrew Birrell, October 20, 1983 9:43 am) Last Edited by: Taft, January 2, 1984 2:38 pm Hal Murray May 17, 1985 2:34:57 am PDT Commands EditedStream.SetMode[stream: cmd.in, echoAsterisks: TRUE]; EditedStream.SetMode[stream: cmd.in, echoAsterisks: FALSE]; Main operations (callable from interpreter) Remembers the passwords of all individual RNames matching pattern. Note: the information is added to the existing tree; call OpenBTree before calling Remember. Compares the passwords of all individual RNames matching pattern with those previously remembered, and also checks that the current password is reasonable. Returns lists of names not passing these tests. A BTree containing the remembered values must already be open; call OpenBTree before calling Compare. Grapevine access and utilities Sending messages BTree access Initialization Κς– "cedar" style˜head™Ibody™ΊLšœS™SL™-Icode™&code2šΟk ˜ Nšœœ ˜Nšœœ˜N˜ N˜N˜Nšœ œ!˜0Nšœ œ!˜2N˜Nšœ ˜ Nšœ œ˜.N˜N˜Nšœ˜N˜N˜Nšœ˜N˜ N˜Nšœ˜N˜ Nšœ ˜ ——šœœ˜NšœΈ˜ΏNšœ˜ Nš˜Nšœœœ˜—™šΟnœ˜-Nš˜˜:Nšœ2œ˜>—Nšœœœ˜Nšœ œœ˜Nš œ œœœœ˜šœœœ˜!šœœ˜Nšœ8œ˜C˜Nšœœ$œ ˜ONšœ%œ˜4—Nšœ œ˜%šœ˜ Nšœ œœ œ ˜/š œœœœœ˜4Nš œ œœœ œ˜:Nšœ˜———Nšœ˜—š œ œœ œ˜$Nšœ"œ˜-—Nšœœœ˜*šœ&œ ˜2Nšœ1œ˜=—š œœœœœœ˜>N˜7N˜,Nšœ˜—N˜ š˜Nšœ˜—Nšœ˜—šžœ˜*Nš˜˜:Nšœ2œ˜>—Nšœœœ˜Nšœ œœ˜Nš œ œœœœ˜šœœœ˜!šœœ˜Nšœ8œ˜C˜Nšœœ%œ˜J—Nšœ œ˜%šœ˜ Nšœ œœ œ ˜/š œœœœœ˜4Nš œ œœœ œ˜:Nšœ˜———Nšœ˜—š œ œœ œ˜$Nšœ"œ˜-—Nšœœœ˜*šœ+˜+Nšœ1œ˜=—š œœœœœœ˜>Nšœœœœ˜+N˜4Nšœg˜gNšœ4˜4Nšœ:˜:Nšœ7˜7Nšœ˜—N˜ š˜Nšœ˜—Nšœ˜—Nš œ-œœœœ˜@šžœœœœ˜GNš˜šœœ˜Nš˜Nšœ œ˜Nšœ+˜+N˜-Nšœ4œ™:Nšœ*˜*Nšœ4œ™;N˜Nšœ>˜>šœœ˜%Nšœ)œœ˜9—N˜Nšœ˜Nšœ˜—Nšœœ˜Nšœ˜—Nšœ œ˜Nšœœœ˜—™+š žœœœœœ˜5N™ŸNš˜š ž œΟc ΠckŸ Ÿ Ÿ Ÿœ˜_Nš˜šœœ˜šœ%˜%NšœC˜C—šœ˜ NšœC˜C——Nšœ˜—šœœœ˜.Nšœ<œ˜D—NšœS˜SNšœ˜—šžœœœœœœ œœœ˜gN™±Nš˜š ž œŸ  Ÿ Ÿ Ÿ Ÿœ˜\Nš˜šœœ˜šœ%˜%Nš˜Nšœœ˜Nšœ˜Nšœ˜Nšœ@˜@šœœ˜NšœG˜GNšœ˜—N˜šœ˜&Nšœœ œ œ˜?Nšœœ˜/Nš œ œ œ œœ œ˜nNšœ˜—Nšœ˜—šœ˜ NšœC˜C——Nšœ˜—Nšœ!œ˜%šœœœ˜.Nšœ<œ˜D—NšœP˜PNšœ˜—šžœœœ$˜^Nš˜Nšœœ ˜$š œœ œœ˜'Nš œ œœœœ œ˜<š˜Nšœœ ˜—Nšœ˜—Nšœ˜—šžœœœœ˜ENš˜Nšœœ ˜$š œœ œœ˜'šœ ˜Nš˜Nšœœ ˜Nš œœœœ œœ˜ENšœ˜Nšœ˜—š˜Nšœœœ˜—Nšœ˜—Nšœ˜—Nš œ œœœœœœ˜Rš žœœœ œœœ˜7Nš˜š œŸ  Ÿ: Ÿ  Ÿ Ÿœ˜Nš˜Nšœ"œ œ˜CNšœ˜—Nšœœ˜ N˜ Nšœ˜—šž œœœœœœœœœ˜ANš˜Nšœœœœ˜šœœ˜ š œœœœ˜(Nšœ œœœ˜,Nšœ˜——Nšœ˜ Nšœ˜——™š ž œœ œœœ˜@Nš˜Nšœœ œœ˜+šœœ˜šœœ˜ Nšœ"˜"Nšœ6˜6Nšœ˜——Nšœ˜Nšœœ ˜Nšœ˜—šžœœœœ˜8šœœœ˜Nšœ˜Nšœ˜Nšœ%˜%Nšœ˜Nšœ"˜"Nšœ˜Nšœ;˜;Nšœ˜Nšœ˜Nšœ1˜1Nšœ˜%——Nšœ œ!˜0Nšž œœœ œœœœ˜Hš ž œœœ-œœ˜_Nš˜Nšœœ˜Nšœ˜šœœ˜šœ˜Nš˜šžœ˜N˜G—N˜.Nšœ9˜9Nšœ˜—šœ˜NšœœŸ6˜DNšœœ˜Nšœ(˜(N˜:šœ˜šœ˜Nšœ˜Nšœ˜Nšœ˜Nšœœ˜—N˜—N˜$šœœ˜˜Nš˜šœ,œœ˜?Nšœ5œ˜=Nšœœœœ˜$Nšœ˜—Nšœ˜—šœ˜ Nšœ\˜\——Nšœ˜—Nšœ˜—Nšœ˜—Nšž œœœœœœœ˜Yš žœœœ.œœ˜bNš˜šž œ˜Nš˜Nšœœ˜Nšœ˜NšœX˜XNšœœœ˜3NšœR˜VNšœ˜—Nšœ:˜:NšœH˜HNšœ˜——™šžœœœ œ˜.Nš˜Nšœ œG˜XNšœ œ˜šžœœœ˜#Nš˜NšœM˜MNšœœœFœ˜\Nšœ˜—š žœœœœœ˜8Nš˜N˜Nšœ œœœ˜3Nšœ˜Nšœ˜—Nšœœœ,œ˜CNšœfœ˜nN˜N˜N˜N˜N˜N˜Nšœhœ˜pNšœjœ˜rN˜2N˜.N˜INšœœœœ˜-Nšœx˜xNšœ;˜ANšœ˜—šž œœœœœ œ œ˜MNš˜N˜(šœœ˜#Nšœ/œ˜6Nšœœ˜—Nšœœ2œ˜RN˜)š œœœœœœ˜@N˜ Nšœ˜—N˜ N˜N˜N˜N˜Nšœ˜—Nšœ˜—Nšœœ˜#N˜Nšœ œ˜šž œœ˜$Nšœœ˜'—šž œœ˜%Nšœœ˜(—šž œœ˜/Nšœ œœ˜6—šžœœœœ˜:Nš˜N˜5Nšœ œœ˜+Nšœ œœœ˜1Nšœ œ œœ$˜FNšœ œœœ˜0Nšœ œœœ_˜Žšœ œœœ˜!Nšœœ˜Nš œœ œ œœJœ˜£Nšœ%œœœ5˜€—Nšœœqœ˜†Nšœœ‡˜‘Nšœ˜Nšœ˜——™ Nšœœ˜Nšœ$˜$š ž œœ œ œœ˜PNš˜Nšœ˜Nšœœ œœ˜%Nšœœœ&œ%˜dNšœb˜bšœœ˜NšœBœβœ˜­—N˜]Nšœ˜—šž œœ˜Nš˜Nšœœœ˜'šœ˜Nš˜Nšœœ˜N˜.šœ˜%Nšœ@˜@—Nšœ ˜ Nšœ˜—Nšœ˜Nšœ˜—š žœœœœ œ<˜rNš˜Nšœ%˜%Nšœœœ!˜?Nšœœœ2˜MNšœ˜—šžœœœ<˜[Nš˜Nšœ%˜%NšœœœM˜hNš œœ œœ œ/˜yNšœ0˜0Nšœ˜—š žœœœœ9œ œ˜ŒNš˜š ž œœœœ œ˜INš˜Nš œœœœœ˜.Nšœ&˜&Nš œœ œœ œ#˜mN˜?Nšœ˜—Nšœ,˜,N˜VNšœ˜—Nšž œœœœ:œ œœ˜ƒšž œŸ Ÿœ ˜YNš˜Nš œœœœœ˜/Nš œœœœœ˜0š œœœœ˜4Nšœœ˜Nšœœ˜Nšœœœ˜Nšœœœ ˜Nšœ˜—Nšœœœ˜(Nšœœœ ˜+Nšœ ˜Nšœ˜—šžœŸ Ÿœ ˜WNš˜Nš œ œœœœœ˜MNšœ˜—šžœŸ Ÿœ ˜cNšœœœ ˜—šžœŸ Ÿœ ˜iNš˜Nš œœœœœ˜0Nšœ œ˜$Nšœœ0œ˜YNšœ˜—š œ œœ œœ˜-N˜NšœŸ˜9Nš œœœ œœœ˜0——™N˜uN˜jNšœ˜——…—DN]ˆ