-- UserCredentialsUnsafeImpl.mesa
-- last edited by Levin on March 11, 1983 11:31 am

DIRECTORY
  Ascii: TYPE USING [
    BS, ControlA, ControlQ, ControlR, ControlV, ControlW, ControlX, CR, DEL, ESC, SP],
  BodyDefs: TYPE USING [maxRNameLength],
  BootSwap: TYPE USING [mdsiGerm],
  DESFace: TYPE USING [Block, DecryptBlock, EncryptBlock, MakeKey],
  DiskChannel: TYPE USING [
    CompletionHandle, Create, CreateCompletionObject, Delete, DiskPageCount,
    DiskPageNumber, Drive, GetDriveAttributes, GetNextDrive, Handle, InitiateIO, IORequest,
    Label, nullDrive, nullHandle, WaitAny],
  Environment: TYPE USING [LongNumber, wordsPerPage],
  File: TYPE USING [ID],
  Heap: TYPE USING [systemMDSZone, systemZone],
  Inline: TYPE USING [BITXOR],
  LongString: TYPE USING [AppendChar, AppendString, EquivalentStrings, StringBoundsFault],
  NameInfoDefs: TYPE USING [Authenticate, Outcome],
  PilotDisk: TYPE USING [],
  PilotMP: TYPE USING [cClient],
  PrincOpsRuntime: TYPE USING [GFT],
  Process: TYPE USING [InitializeMonitor, Pause, SecondsToTicks],
  ProcessorFace: TYPE USING [SetMP],
  SDDefs: TYPE USING [SD, sGFTLength],
  Space: TYPE USING [
    Create, Delete, GetHandle, Handle, LongPointer, Map, PageFromLongPointer, virtualMemory],
  SpecialSpace: TYPE USING [MakeResident, MakeSwappable],
  System: TYPE USING [UniversalID],
  UserCredentialsUnsafe: TYPE USING [defaultOptions, GetProc, LoginOptions, PutProc, State];
  
UserCredentialsUnsafeImpl: MONITOR
  IMPORTS 
    DESFace, DiskChannel, Heap, Inline, NameInfoDefs, Process, ProcessorFace,
    Space, SpecialSpace, String: LongString
  EXPORTS UserCredentialsUnsafe
  SHARES PilotDisk -- to get at structure of a disk label -- =

BEGIN


IllegalParameter: ERROR = CODE;

PanelCode: TYPE = CARDINAL;
bug: PanelCode = 998;  -- = CedarInitPrivate.ErrorCode[implementationBug]
hardDiskError: PanelCode = 992;
diskOffline: PanelCode = 993;


-- Declarations for data structures on disk

State: TYPE = UserCredentialsUnsafe.State;

credentialsImplID: CARDINAL = 08312;
obsoleteImplID: CARDINAL = credentialsImplID - 1;  -- anything other than credentialsImplID

decryptedID: DESFace.Block = ALL[credentialsImplID];

Credentials: TYPE = LONG POINTER TO CredentialsObject;
CredentialsObject: TYPE = MACHINE DEPENDENT RECORD [
  implementationID(0): CARDINAL ← credentialsImplID,
  checksum(1): CARDINAL ← 0,
  fill(2:0..13): CARDINAL[0..17777B] ← 0,
  kind(2:14..14+2+6*16-1): SELECT state(2:14..15): State FROM
    noCredentials => NULL,
    name => [
        encryptedID(3): DESFace.Block ← NULL,
        userName(7): StringBody ← [length: 0, maxlength: NULL, text: NULL]
        -- The text portion of the userName follows here
        ],
    nameHint, nameAndPassword => [
        userName(3): StringBody ← [length: 0, maxlength: NULL, text: NULL]
        -- The text portion of the userName follows here
        -- If state = nameAndPassword, another StringBody follows
        ],
    ENDCASE
  ];

bogusCredentials: CredentialsObject = [implementationID: obsoleteImplID, kind: noCredentials[]];

diskCredentialsPage: DiskChannel.DiskPageNumber = 2;  -- should come from DiskFace or somewhere
diskCredentialsPages: DiskChannel.DiskPageCount = 
  (SIZE[nameAndPassword CredentialsObject] - SIZE[StringBody[0]] +
   2*SIZE[StringBody[BodyDefs.maxRNameLength]]
   + Environment.wordsPerPage - 1)/Environment.wordsPerPage;

UniversalID: TYPE = ARRAY [0..SIZE[System.UniversalID]) OF WORD;

credentialsFileID: File.ID =
  LOOPHOLE[UniversalID[076543B, 021076B, 054321B, 076543B, 021076B]];
credentialsLabel: DiskChannel.Label = [
  fileID: credentialsFileID, filePageLo: 0, filePageHi: 0,
  immutable: FALSE, temporary: FALSE, zeroSize: FALSE,
  type: [0], bootChainLink: [0, 0]];

DiskNotReady: ERROR = CODE;
BrokenDisk: ERROR = CODE;


-- Declarations for data structures in memory (germ)


GermString: TYPE = LONG POINTER TO LONG STRING;

germMDSln: Environment.LongNumber = [num[highbits: BootSwap.mdsiGerm, lowbits: 0]];
germMDS: LONG POINTER = germMDSln.lp;  -- compiler coughs if combined with above.
germGFT: LONG POINTER = LOOPHOLE[germMDS+LOOPHOLE[PrincOpsRuntime.GFT, CARDINAL]];
germSD: LONG POINTER TO ARRAY [0..0) OF UNSPECIFIED =
  LOOPHOLE[germMDS+LOOPHOLE[SDDefs.SD, CARDINAL]];
nameInGerm: GermString = LOOPHOLE[germGFT+LOOPHOLE[germSD[SDDefs.sGFTLength], CARDINAL]];
passwordInGerm: GermString = nameInGerm + SIZE[LONG STRING];
germFreeSpace: LONG POINTER = passwordInGerm + SIZE[LONG STRING];
germFreeSpaceCount: CARDINAL =
  (germSD[SDDefs.sGFTLength] + Environment.wordsPerPage - 1)/Environment.wordsPerPage -
  (germSD[SDDefs.sGFTLength] + 2*SIZE[LONG STRING]);



-- Exports to UserCredentialsUnsafe

Login: PUBLIC ENTRY PROC [
  startInteraction: PROC RETURNS [UserCredentialsUnsafe.GetProc, UserCredentialsUnsafe.PutProc],
  endInteraction: PROC,
  options: UserCredentialsUnsafe.LoginOptions ← UserCredentialsUnsafe.defaultOptions] = {
  ENABLE UNWIND => NULL;
  credentials: Credentials ← NIL;
  dirty: BOOL ← FALSE;
  terminalOn: BOOL ← FALSE;
  EnsureTerminalOn: PROC =
    {IF ~terminalOn THEN {[getChar, putChar] ← startInteraction[]; terminalOn ← TRUE}};
  EnsureTerminalOff: PROC =
    {IF terminalOn THEN endInteraction[]};
  HandleDiskNotReady: PROC = {
    ProcessorFace.SetMP[diskOffline];
    EnsureTerminalOn[];
    PutString["Disk is not ready...will retry..."L];
    Process.Pause[Process.SecondsToTicks[5]];
    PutString["retrying\N"L];
    ProcessorFace.SetMP[PilotMP.cClient];
    };
  CleanupOnAbort: INTERNAL PROC = {
    IF ~options.ignoreDiskEntirely THEN ReleaseDiskCredentials[credentials, FALSE];
    FreeCredentials[credentials];
    SetUserCredentialsInternal[NIL, NIL];
    };
  EnsureInitialized[];
  IF ~options.alwaysPrompt AND nameInGerm↑.length > 0 THEN RETURN;  -- already logged in
  credentials ← AllocateCredentials[];
  IF options.ignoreDiskEntirely THEN credentials↑ ← bogusCredentials
  ELSE AcquireDiskCredentials[credentials ! DiskNotReady => {HandleDiskNotReady[]; RETRY}];
  IF credentials.implementationID = credentialsImplID THEN {
    CheckCredentials: INTERNAL PROC [credentials: Credentials]
      RETURNS [ok: BOOL ← TRUE] = --INLINE-- {
      Downgrade: INTERNAL PROC [credentials: Credentials, state: State] = --INLINE--
        {[] ← ChangeCredentialsStateInternal[credentials, state]; dirty ← TRUE; ok ← FALSE};
      WITH cred: credentials SELECT FROM
        nameHint => {
          diskUserName: LONG STRING = @cred.userName;
          EnsureTerminalOn[];
          SetUserCredentialsInternal[diskUserName, NIL];
          [] ← GetAndAuthenticate[ ! UNWIND => CleanupOnAbort[]];
          IF (dirty ← ~String.EquivalentStrings[diskUserName, nameInGerm↑]) THEN
            [] ← ChangeCredentialsStateInternal[credentials, nameHint];
          };
        nameAndPassword => {
          diskUserName: LONG STRING = @cred.userName;
          diskPassword: LONG STRING = diskUserName + SIZE[StringBody[diskUserName.length]];
          SetUserCredentialsInternal[diskUserName, diskPassword];
          SELECT AuthenticateInternal[diskUserName, diskPassword] FROM
            individual, allDown => NULL;
            notFound, group => Downgrade[credentials, nameHint];
            badPwd => Downgrade[credentials, name]; 
            ENDCASE => HardStop[bug];
          };
        name => {
          EnsureTerminalOn[];
          SetUserCredentialsInternal[@cred.userName, NIL];
          DO  -- loops only if GV down and password supplied doesn't work.
            block: DESFace.Block;
            SELECT GetAndAuthenticate[passwordOnly ! UNWIND => CleanupOnAbort[]] FROM
              ok => {
                -- Grapevine is up, and the credentials are valid.  The password used to encrypt
                -- the information in the credentials file may not be the same one that was used
                -- (successfully) to authenticate the user to Grapevine.  After all, the user may
                -- have changed his password in the Grapevine database since this procedure
                -- was last called.  However, since the user name presented to Grapevine
                -- was forced to come from the disk and Grapevine authenticated the user, we
                -- can be confident that this is the same user who requested that the encrypted
                -- information be stored on the disk originally.  We therefore encrypt with
                -- the new password and rewrite the credentials file.  In effect, this gives
                -- us a cache of the last successful Grapevine authentication, which will be
                -- useful if Grapevine proves to be down on a subsequent Login.
                DESFace.DecryptBlock[
                  key: DESFace.MakeKey[passwordInGerm↑],
                  from: @cred.encryptedID, to: @block];
                IF (dirty ← dirty OR (block ~= decryptedID)) THEN
                  [] ← ChangeCredentialsStateInternal[credentials, name];
                };
              bogusGVName => Downgrade[credentials, nameHint];
              gvDown => {
                -- Grapevine is down, so we must fall back on our cached information on
                -- the disk.  We use the password supplied by the user to decrypt then compare 
                -- it with the expected value.  If it matches, the user is assumed to be
                -- authentic, and the password he supplied is highly likely to be the one
                -- he last successfully used to authenticate himself with Grapevine.
                DESFace.DecryptBlock[
                  key: DESFace.MakeKey[passwordInGerm↑],
                  from: @cred.encryptedID, to: @block];
                IF block ~= decryptedID THEN {PutString["Wrong credentials!\N"L]; LOOP};
                };
              ENDCASE;
            EXIT
            ENDLOOP;
          };
        ENDCASE => HardStop[bug];
      };
    UNTIL CheckCredentials[credentials] DO ENDLOOP;
    }
  ELSE {
    ENABLE UNWIND => CleanupOnAbort[];
    RewriteDisk: PROCEDURE RETURNS [rewrite: BOOLEAN] = INLINE {
      IF options.ignoreDiskEntirely THEN RETURN[FALSE];
      IF ~options.confirmCredentialsOverwrite THEN RETURN[TRUE];
      RETURN[~Confirm["Disk credentials missing or obsolete; shall I leave it that way? "L]]
      };
    EnsureTerminalOn[];
    IF (dirty ← RewriteDisk[]) THEN
      [] ← ChangeCredentialsStateInternal[credentials,
        IF ~options.prohibitDiskProtection AND GetAndAuthenticate[] = ok AND
          Confirm["Do you wish to prevent others from accessing your disk? "L] THEN name
        ELSE nameHint]
    ELSE [] ← GetAndAuthenticate[];
    };
  IF ~options.ignoreDiskEntirely THEN 
    ReleaseDiskCredentials[credentials, dirty
      ! BrokenDisk => {
        EnsureTerminalOn[];
        PutString["Disk error rewriting credentials file...I give up\N"L];
        HardStop[hardDiskError];
        };
        DiskNotReady => {HandleDiskNotReady[]; RETRY};
      ];
  FreeCredentials[credentials];
  EnsureTerminalOff[];
  };

GetCredentialsState: PUBLIC ENTRY PROC RETURNS [state: State] = {
  credentials: Credentials = AllocateCredentials[];
  AcquireDiskCredentials[credentials];
  state ← GetCredentialsStateInternal[credentials];
  ReleaseDiskCredentials[credentials, FALSE];
  FreeCredentials[credentials];
  };

ChangeCredentialsState: PUBLIC ENTRY PROC [new: State] RETURNS [old: State] = {
  -- The internal signals from AcquireDiskCredentials and ReleaseDiskCredentials are
  -- presently uncaught, since we don't have any better way of dealing with them.
  credentials: Credentials = AllocateCredentials[];
  AcquireDiskCredentials[credentials];
  old ← ChangeCredentialsStateInternal[credentials, new];
  ReleaseDiskCredentials[credentials, TRUE];
  FreeCredentials[credentials];
  };

GetUserCredentials: PUBLIC ENTRY PROC [name, password: LONG STRING] = {
  EnsureInitialized[];
  GetUserCredentialsInternal[name, password];
  };

SetUserCredentials: PUBLIC ENTRY PROC [name, password: LONG STRING] =
  -- The implementation of SetUserCredentialsInternal is such that we need not call
  -- EnsureInitialized (indeed, note how EnsureInitialized is implemented).
  {SetUserCredentialsInternal[name, password]};

Authenticate: PUBLIC ENTRY PROC RETURNS [outcome: NameInfoDefs.Outcome] = {
  EnsureInitialized[];
  RETURN[AuthenticateInternal[nameInGerm↑, passwordInGerm↑]]
  };

-- *** For emergency use only ***

BreakIn: PROC = {
  BreakInInternal: ENTRY PROC = INLINE {
    credentials: CredentialsObject;
    [] ← ChangeCredentialsStateInternal[@credentials, noCredentials];
    ReleaseDiskCredentials[@credentials, TRUE];
    };
  Process.InitializeMonitor[@LOCK];
  BreakInInternal[];
  };

-- Internal procedures

EnsureInitialized: INTERNAL PROC =
  -- Note: one might think that this could be done at module start time, rather than
  -- on every call to a public procedure of this module.  However, the contents of the
  -- germ mds can change after this module is started (e.g., a checkpoint is taken, then
  -- a physical boot causes a rollback).
  {IF nameInGerm↑ = NIL THEN SetUserCredentialsInternal[NIL, NIL]};

GetUserCredentialsInternal: INTERNAL PROC [name, password: LONG STRING] = {
  IF name ~= NIL THEN {name.length ← 0; String.AppendString[name, nameInGerm↑]};
  IF password ~= NIL THEN {password.length ← 0; String.AppendString[password, passwordInGerm↑]};
  };

SetUserCredentialsInternal: INTERNAL PROC [name, password: LONG STRING] = {
  empty: LONG STRING ← [0];
  IF name = NIL THEN name ← empty;
  IF password = NIL THEN password ← empty;
  IF SIZE[StringBody[name.length]] + SIZE[StringBody[password.length]] > germFreeSpaceCount OR
    name.length > BodyDefs.maxRNameLength OR password.length > BodyDefs.maxRNameLength THEN
    ERROR IllegalParameter;
  nameInGerm↑ ← germFreeSpace;
  nameInGerm↑↑ ← StringBody[maxlength: name.length, text: ];
  String.AppendString[nameInGerm↑, name];
  passwordInGerm↑ ← nameInGerm↑ + SIZE[StringBody[name.length]];
  passwordInGerm↑↑ ← StringBody[maxlength: password.length, text: ];
  String.AppendString[passwordInGerm↑, password];
  };

GetCredentialsStateInternal: INTERNAL PROC [credentials: Credentials]
  RETURNS [State] = INLINE {RETURN[credentials.state]};

ChangeCredentialsStateInternal: INTERNAL PROC [credentials: Credentials, new: State]
  RETURNS [old: State] = {
  words: CARDINAL;
  old ← credentials.state;
  SELECT new FROM
    noCredentials => {
      credentials↑ ← bogusCredentials;
      words ← SIZE[noCredentials CredentialsObject];
      };
    name => {
      credentials↑ ←
        [kind: name[encryptedID: , userName: StringBody[maxlength: nameInGerm↑.length, text: ]]];
      WITH cred: credentials SELECT FROM
        name => {
          block: DESFace.Block ← decryptedID;
          DESFace.EncryptBlock[
            key: DESFace.MakeKey[passwordInGerm↑],
            from: @block, to: @cred.encryptedID];
          String.AppendString[@cred.userName, nameInGerm↑];
          };
        ENDCASE => HardStop[bug];
      words ← SIZE[name CredentialsObject] +
        (SIZE[StringBody[nameInGerm↑.length]] - SIZE[StringBody[0]]);
      };
    nameHint => {
      credentials↑ ←
        [kind: nameHint[userName: StringBody[maxlength: nameInGerm↑.length, text: ]]];
      WITH cred: credentials SELECT FROM
        nameHint => String.AppendString[@cred.userName, nameInGerm↑];
        ENDCASE => HardStop[bug];
      words ← SIZE[nameHint CredentialsObject] +
        (SIZE[StringBody[nameInGerm↑.length]] - SIZE[StringBody[0]]);
      };
    nameAndPassword => {
      credentials↑ ←
        [kind: nameAndPassword[userName: StringBody[maxlength: nameInGerm↑.length, text: ]]];
      WITH cred: credentials SELECT FROM
        nameAndPassword => {
          diskUserName: LONG STRING = @cred.userName;
          diskPassword: LONG STRING = @cred.userName + SIZE[StringBody[nameInGerm↑.length]];
          String.AppendString[@cred.userName, nameInGerm↑];
          diskPassword↑ ← StringBody[maxlength: passwordInGerm↑.length, text: ];
          String.AppendString[diskPassword, passwordInGerm↑];
          };
        ENDCASE => HardStop[bug];
      words ← SIZE[nameAndPassword CredentialsObject] +
        (SIZE[StringBody[nameInGerm↑.length]] - SIZE[StringBody[0]]) +
        SIZE[StringBody[passwordInGerm↑.length]];
      };
    ENDCASE => HardStop[bug];
  credentials.checksum ← ComputeChecksum[DESCRIPTOR[credentials, words]];
  };

ComputeChecksum: INTERNAL PROCEDURE [input: LONG DESCRIPTOR FOR ARRAY OF WORD]
  RETURNS [checksum: WORD ← 0] = {
  FOR i: CARDINAL IN [0..LENGTH[input]) DO
    checksum ← Inline.BITXOR[checksum, input[i]];
    ENDLOOP;
  };

ValidChecksum: INTERNAL PROCEDURE [input: LONG DESCRIPTOR FOR ARRAY OF WORD]
  RETURNS [ok: BOOLEAN] = {
  checksum: WORD ←  0;
  FOR i: CARDINAL IN [0..LENGTH[input]) DO
    checksum ← Inline.BITXOR[checksum, input[i]];
    ENDLOOP;
  RETURN[checksum = 0]
  };

AuthenticateInternal: INTERNAL PROC [name, password: LONG STRING]
  RETURNS [outcome: NameInfoDefs.Outcome] = {
  nameMDS: STRING ← Heap.systemMDSZone.NEW[StringBody[name.length]];
  passwordMDS: STRING ← Heap.systemMDSZone.NEW[StringBody[password.length]];
  String.AppendString[nameMDS, name];
  String.AppendString[passwordMDS, password];
  outcome ← NameInfoDefs.Authenticate[nameMDS, passwordMDS];
  Heap.systemMDSZone.FREE[@nameMDS];
  Heap.systemMDSZone.FREE[@passwordMDS];
  };

GetAndAuthenticate: INTERNAL PROCEDURE [askFor: {both, passwordOnly} ← both]
  RETURNS [outcome: {ok, bogusGVName, gvDown} ← ok] = {
  name: LONG STRING ← Heap.systemZone.NEW[StringBody[BodyDefs.maxRNameLength]];
  password: LONG STRING ← Heap.systemZone.NEW[StringBody[BodyDefs.maxRNameLength]];
  defaultRegistry: LONG STRING = ".pa"L;
  cancelString: LONG STRING = " XXX\N"L;
  invalidMsg: LONG STRING = " too long!\N"L;
  PutString["Login please...\N"L];
  DO
    ENABLE UNWIND => PutString[cancelString];
    PutString["Name: "L];
    GetUserCredentialsInternal[name: name, password: NIL];
    IF askFor = passwordOnly THEN PutString[name]
    ELSE {
      [] ← GetID[name
        ! Rubout => {PutString[cancelString]; LOOP};
          String.StringBoundsFault => {PutString[invalidMsg]; LOOP};
        ];
      FOR i: CARDINAL DECREASING IN [0..name.length) DO
        IF name[i] = '. THEN EXIT;
        REPEAT
          FINISHED => {
            PutString[defaultRegistry];
            String.AppendString[name, defaultRegistry
              ! String.StringBoundsFault => {PutString[invalidMsg]; LOOP}];
            };
        ENDLOOP;
      };
    PutString["  password: "L];
    password.length ← 0;
    [] ← GetID[password, FALSE
      ! Rubout => {PutString[cancelString]; LOOP};
        String.StringBoundsFault => {PutString[invalidMsg]; LOOP};
      ];
    SetUserCredentialsInternal[name, password];
    PutString[" GV..."L];
    SELECT AuthenticateInternal[name, password] FROM
      individual => {PutString["ok\N"L]; EXIT};
      notFound, group => {
        PutString["invalid user name"L];
        IF askFor = passwordOnly THEN {PutString["\N"L]; outcome ← bogusGVName; EXIT};
        };
      badPwd => PutString["incorrect password"L];
      allDown => {PutString["Grapevine down, proceeding anyway\N"L]; outcome ← gvDown; EXIT};
      ENDCASE;
    PutString[", try again.\N"L];
    ENDLOOP;
  Heap.systemZone.FREE[@name];
  Heap.systemZone.FREE[@password];
  };

-- TTY-like Interaction Utilities

Rubout: ERROR = CODE;

getChar: UserCredentialsUnsafe.GetProc;  -- used for the duration of Login only
putChar: UserCredentialsUnsafe.PutProc;  -- used for the duration of Login only

PutString: PROC [s: LONG STRING] =
  {FOR i: CARDINAL IN [0..s.length) DO putChar[s[i]]; ENDLOOP};

GetID: PROC [s: LONG STRING, echo: BOOL ← TRUE] RETURNS [c: CHARACTER] =
  BEGIN OPEN Ascii;
  firstTime: BOOLEAN ← TRUE;
  Done: PROCEDURE [c: CHARACTER] RETURNS [yes: BOOLEAN] = INLINE {
    IF firstTime AND echo THEN {
      SELECT c FROM
        ControlA, BS, ControlQ, ControlW, ControlX, CR, SP => NULL;
        ENDCASE => {
          THROUGH [0..s.length) DO putChar[BS] ENDLOOP;
          s.length ← 0};
      firstTime ← FALSE};
    RETURN[c = SP OR c = CR]
    };
  IF echo THEN PutString[s];
  c ← getChar[];
  UNTIL Done[c] DO
    SELECT c FROM
      DEL => ERROR Rubout;
      ControlA, BS => -- backspace
        IF s.length > 0 THEN {IF echo THEN putChar[BS]; s.length ← s.length - 1};
      ControlW, ControlQ => {-- backword
        -- text to be backed up is of the form ...<li><v><ti>, the <v> and <ti> are to
        -- be removed.
        state: {ti, v, li} ← ti;
        FOR i: CARDINAL DECREASING IN [0..s.length) DO
          SELECT s[i] FROM
            IN ['A..'Z], IN ['a..'z], IN ['0..'9] => IF state = ti THEN state ← v;
            ENDCASE => IF state = v THEN state ← li;
          IF state = li THEN GO TO done;
          IF echo THEN putChar[BS];
          REPEAT
            done => s.length ← i + 1;
            FINISHED => s.length ← 0;
          ENDLOOP;
        };
      ControlX => {-- back everything
        IF echo THEN FOR i: CARDINAL IN [0..s.length) DO putChar[BS] ENDLOOP;
        s.length ← 0;
        };
      ControlR => -- refresh -- IF echo THEN {putChar[CR]; PutString[s]};
      ControlV => -- quote next char
        {String.AppendChar[s, c ← getChar[]]; IF echo THEN putChar[c]};
      ENDCASE => {String.AppendChar[s, c]; IF echo THEN putChar[c]};
    c ← getChar[];
    ENDLOOP;
  END;


Confirm: PROCEDURE [message: LONG STRING ← NIL] RETURNS [BOOLEAN] = {
  DO
    IF message ~= NIL THEN PutString[message];
    SELECT getChar[ ! UNWIND => PutString[" XXX\N"L]] FROM
      'y, 'Y, Ascii.SP, Ascii.CR, Ascii.ESC => {PutString["Yes\N"L]; RETURN[TRUE]};
      'n, 'N, Ascii.DEL => {PutString["No\N"L]; RETURN[FALSE]};
      ENDCASE;
    ENDLOOP};

channel: DiskChannel.Handle ← DiskChannel.nullHandle;
completion: DiskChannel.CompletionHandle;
rewriteLabel: BOOL;

AllocateCredentials: INTERNAL PROC RETURNS [credentials: Credentials] = {
  space: Space.Handle = Space.Create[size: diskCredentialsPages, parent: Space.virtualMemory];
  credentials ← Space.LongPointer[space];
  -- Unfortunately, the following consumes unnecessary backing storage, but it isn't much.
  Space.Map[space];
  SpecialSpace.MakeResident[space];
  };

FreeCredentials: INTERNAL PROC [credentials: Credentials] = {
  space: Space.Handle = Space.GetHandle[Space.PageFromLongPointer[credentials]];
  SpecialSpace.MakeSwappable[space];
  Space.Delete[space];
  };

AcquireDiskCredentials: INTERNAL PROC [credentials: Credentials] = {
  request: DiskChannel.IORequest;
  label: DiskChannel.Label ← credentialsLabel;
  IF channel ~= DiskChannel.nullHandle THEN HardStop[bug];
  channel ← DiskChannel.Create[GetDrive[], completion ← DiskChannel.CreateCompletionObject[]];
  -- I'd like to use a constructor, but the declaration isn't conducive (PRIVATE fields)
  request.command ← vvr;
  request.channel ← channel;
  request.diskPage ← diskCredentialsPage;
  request.memoryPage ← Space.PageFromLongPointer[credentials];
  request.count ← diskCredentialsPages;
  request.label ← @label;
  request.dontIncrement ← FALSE;
  DiskChannel.InitiateIO[@request];
  IF DiskChannel.WaitAny[completion] ~= @request THEN HardStop[bug];
  rewriteLabel ← FALSE;
  SELECT request.status FROM
    goodCompletion => {
      words: CARDINAL;
      WITH cred: credentials SELECT FROM
        noCredentials => words ← SIZE[noCredentials CredentialsObject];
        name =>
          words ← SIZE[name CredentialsObject] +
            (SIZE[StringBody[cred.userName.length]] - SIZE[StringBody[0]]);
        nameHint =>
          words ← SIZE[nameHint CredentialsObject] +
            (SIZE[StringBody[cred.userName.length]] - SIZE[StringBody[0]]);
        nameAndPassword => {
          diskUserName: LONG STRING = @cred.userName;
          diskPassword: LONG STRING = @cred.userName + SIZE[StringBody[diskUserName.length]];
          words ← SIZE[nameAndPassword CredentialsObject] +
            (SIZE[StringBody[diskUserName.length]] - SIZE[StringBody[0]]) +
            SIZE[StringBody[diskPassword.length]];
          };
        ENDCASE;
      IF ~ValidChecksum[DESCRIPTOR[credentials, words]] THEN credentials↑ ← bogusCredentials;
      };
    labelDoesNotMatch, labelError =>
      {rewriteLabel ← TRUE; credentials↑ ← bogusCredentials};
    seekFailed, dataError, hardwareError =>
      -- The disk is there, but it doesn't seem to work, treat as "no credentials" now.
      credentials↑ ← bogusCredentials;
    notReady => {CleanupIO[]; ERROR DiskNotReady};
    noSuchPage, notPilotVolume, invalidChannel, checkError => HardStop[bug];
    ENDCASE;
  };

ReleaseDiskCredentials: INTERNAL PROC [credentials: Credentials, dirty: BOOL] = {
  IF dirty THEN {
    request: DiskChannel.IORequest;
    label: DiskChannel.Label ← credentialsLabel;
    IF channel = DiskChannel.nullHandle THEN HardStop[bug];
    -- I'd like to use a constructor, but the declaration isn't conducive (PRIVATE fields)
    request.command ← IF rewriteLabel THEN vww ELSE vvw;
    request.channel ← channel;
    request.diskPage ← diskCredentialsPage;
    request.memoryPage ← Space.PageFromLongPointer[credentials];
    request.count ← diskCredentialsPages;
    request.label ← @label;
    request.dontIncrement ← FALSE;
    DiskChannel.InitiateIO[@request];
    IF DiskChannel.WaitAny[completion] ~= @request THEN HardStop[bug];
    SELECT request.status FROM
      goodCompletion => NULL;
      labelDoesNotMatch => HardStop[bug];
      seekFailed, labelError, dataError, hardwareError => {CleanupIO[]; ERROR BrokenDisk};
      notReady => ERROR DiskNotReady;
      noSuchPage, notPilotVolume, invalidChannel, checkError => HardStop[bug];
      ENDCASE;
    };
  CleanupIO[];
  };

CleanupIO: INTERNAL PROC =
  {DiskChannel.Delete[channel]; channel ← DiskChannel.nullHandle};

GetDrive: PROC RETURNS [drive: DiskChannel.Drive ← DiskChannel.nullDrive] = {
  -- At present, this is rather a crock.  It returns a disk channel for the first drive
  -- holding an online, asserted-Pilot physical volume.
  WHILE (drive ← DiskChannel.GetNextDrive[drive]) ~= DiskChannel.nullDrive DO
    IF DiskChannel.GetDriveAttributes[drive].state = pilot THEN EXIT;
    ENDLOOP;
  };

HardStop: PROC [panelCode: PanelCode] = {
  ProcessorFace.SetMP[panelCode];
  DO ENDLOOP;
  };


-- Start code

Initialize: ENTRY PROC = {EnsureInitialized[]};

Initialize[];

END.