-- File: AltoDirectory.mesa
-- Last edited by Levin:  24-Aug-82 14:08:39

DIRECTORY
  AltoDefs USING [BytesPerWord, PageSize],
  AltoFile USING [
    CFP, CreateFile, DEfile, DEfree, DV, DVPtr, fileNameChars, FP, FPPtr, LookupAction,
    VersionOption],
  AltoFilePrivate USING [DirHandle, DirObject],
  DiskIODefs USING [fillInvDA, vDA],
  FileDefs USING [ComparePositions, IncrementPosition, Position],
  Inline USING [COPY],
  StringDefs USING [
    AppendChar, BcplSTRING, BcplToMesaString, EquivalentString,
    MesaToBcplString, StringBoundsFault, WordsForBcplString],
  VMDefs USING [
    FileHandle, GetFileLength, Mark, MarkStart, Page, PageAddress, PageNumber,
    Position, ReadPage, Release, Start, SetFileLength, WaitFile];

AltoDirectory: MONITOR LOCKS dir.LOCK USING dir: AltoFilePrivate.DirHandle
  IMPORTS AltoFile, FileDefs, Inline, StringDefs, VMDefs
  EXPORTS AltoFile, AltoFilePrivate =

  BEGIN OPEN AltoFile, AltoFilePrivate, FileDefs;

  -- Miscellaneous Declarations --

  LookupType: TYPE = {name, fp};

  BadDirectory: ERROR = CODE;

  bcplNameSize: CARDINAL = -- WordsForBcplString[fileNameChars] -- 20;

  DEugly: CARDINAL = DEfile + 1;


  -- Types Exported to AltoFile --

  DirObject: PUBLIC TYPE = AltoFilePrivate.DirObject;


  -- Procedures and Signals Exported to AltoFile --

  MapFileNameToFP: PUBLIC PROCEDURE [name: STRING, version: VersionOption,
    leadervDA: DiskIODefs.vDA ← DiskIODefs.fillInvDA]
    RETURNS [fp: FP] =
    BEGIN
    SELECT version FROM
      old => IF ~Lookup[sysDir, name, @fp].found THEN ERROR NoSuchFile;
      new =>
	IF ~Lookup[sysDir, name, @fp, [new[leadervDA]]].new THEN
	  ERROR FileAlreadyExists;
      oldOrNew => [] ← Lookup[sysDir, name, @fp, [new[leadervDA]]];
      ENDCASE;
    END;

  NoSuchFile: PUBLIC ERROR = CODE;
  FileAlreadyExists: PUBLIC ERROR = CODE;
  IllegalFileName: PUBLIC ERROR = CODE;

  Enumerate: PUBLIC ENTRY PROCEDURE [
    dir: DirHandle, proc: PROCEDURE [entry: DVPtr, name: STRING] RETURNS [BOOLEAN]]
    RETURNS [found: BOOLEAN] =
    BEGIN

    PassItOn: PROCEDURE [entry: DVPtr, entryName: STRING, entryPos: Position]
      RETURNS [BOOLEAN] = {RETURN[proc[entry, entryName]]};

    found ← DoEnumerate[dir, PassItOn ! UNWIND => NULL];
    END;

  Lookup: PUBLIC PROCEDURE [
    dir: DirHandle, name: STRING, fp: FPPtr, action: LookupAction ← [old[]]]
    RETURNS [found, new: BOOLEAN] =
    BEGIN
    fullName: STRING ← [fileNameChars];

    DoIt: ENTRY PROCEDURE[dir: DirHandle] =
      BEGIN
      [found, ] ← DoLookup[dir, name, fullName, fp];
      IF ~found THEN
	WITH a: action SELECT FROM
	  old => NULL;
	  new =>
	    BEGIN
	    new ← TRUE;
	    fp↑ ← CreateFile[fullName, dir.fp, a.leadervDA];
	    InsertEntry[dir, fullName, fp];
	    END;
	  ENDCASE;
     END;

    ValidateAndExpandName[fullName, name];
    new ← FALSE;
    DoIt[dir ! UNWIND => NULL];
    END;

  LookupFP: PUBLIC ENTRY PROCEDURE [dir: DirHandle, fp: FPPtr, name: STRING]
    RETURNS [found: BOOLEAN] =
    {[found, ] ← DoLookup[dir, fp, name, fp ! UNWIND => NULL]};

  Enter: PUBLIC PROCEDURE [dir: DirHandle, name: STRING, fp: FPPtr]
    RETURNS [entered: BOOLEAN] =
    BEGIN
    fullName: STRING ← [fileNameChars];

    DoIt: ENTRY PROCEDURE [dir: DirHandle] =
      BEGIN
      sinkFP: FP;
      IF (entered ← ~DoLookup[dir, name, fullName, @sinkFP].found) THEN
	InsertEntry[dir, fullName, fp];
      END;

    ValidateAndExpandName[fullName, name];
    DoIt[dir ! UNWIND => NULL];
    END;

  Delete: PUBLIC PROCEDURE [dir: DirHandle, name: STRING, fp: FPPtr ← NIL]
    RETURNS [found: BOOLEAN] =
    BEGIN
    fullName: STRING ← [fileNameChars];
    sinkFP: FP;
    ValidateAndExpandName[fullName, name];
    RETURN[DoDelete[dir, name, fullName, IF fp = NIL THEN @sinkFP ELSE fp]]
    END;

  DeleteFP: PUBLIC PROCEDURE [dir: DirHandle, fp: FPPtr, name: STRING ← NIL]
    RETURNS [found: BOOLEAN] =
    BEGIN
    sink: STRING = [0];
    RETURN[DoDelete[dir, fp, IF name = NIL THEN sink ELSE name, fp]]
    END;


  -- Procedures and Signals Exported to AltoFilePrivate --

  sysDir: PUBLIC DirHandle;

  FlushDirectoryBuffer: PUBLIC PROCEDURE [dir: DirHandle] =
    -- eliminates any cached page buffer.
    BEGIN
    IF dir.buffer ~= NIL THEN
      BEGIN
      VMDefs.Release[dir.buffer];
      dir.buffer ← NIL;
      dir.page ← LAST[VMDefs.PageNumber];
      END;
    END;

  ResetDirectoryLength: PUBLIC PROCEDURE [dir: DirHandle] =
    -- recomputes 'dir.length'.
    BEGIN
    position: VMDefs.Position = VMDefs.GetFileLength[dir.file];
    dir.length ← [position.page, position.byte];
    END;


  -- Internal Procedures --

  -- Lookup, Enter, and Delete Procedures --

  DoLookup: PROCEDURE [dir: DirHandle, type: LookupType, name: STRING, fp: FPPtr]
    RETURNS [found: BOOLEAN, pos: Position] =
    -- If type = name, looks up the given 'name' in 'dir'.  If the name is found, fp↑
    -- is filled in with the associated file pointer from the directory, 'found'
    -- becomes TRUE, and 'pos' indicates the location of the entry in the directory.  If
    -- the name is not found, 'fp' is unchanged, 'found' is FALSE, 'pos' is undefined,
    -- and <dir.spaceFA, dir.spaceFound> is a block of free storage in the directory of
    -- possible interest to InsertEntry.  If dir.spaceFound is greater than or equal to
    -- dir.spaceNeeded, the block is wholly contained within the existing directory.  If
    -- not, the directory will require extension to accommodate a new entry of size
    -- dir.spaceNeeded.  If type = fp, this procedure looks up fp↑ in 'dir'.  If a
    -- matching entry is found, 'name' has the associated file name stored in it,
    -- 'found' becomes TRUE, and 'pos' indicates the location of the entry in the
    -- directory.  If fp↑ is not found, 'name' is unchanged, 'found' is FALSE and
    -- 'pos' is undefined.
    BEGIN

    CheckName: PROCEDURE [entry: DVPtr, entryName: STRING, entryPos: Position]
      RETURNS [BOOLEAN] =
      -- matches a file name entry against fullName.  A match terminates the
      -- enumeration.  As a side effect, this procedure records the location of a free
      -- slot of adequate size to hold the name.
      BEGIN
      match: BOOLEAN ← FALSE;
      SELECT entry.type FROM
	DEfree =>
	  BEGIN
	  IF dir.spaceFound = 0 THEN dir.spacePos ← entryPos;
	  IF dir.spaceFound < dir.spaceNeeded THEN
	    dir.spaceFound ← dir.spaceFound + entry.length;
	  END;
	DEfile =>
	  IF StringDefs.EquivalentString[name, entryName] THEN
	    {fp↑ ← FP[entry.fp.serial, entry.fp.leaderDA]; pos ← entryPos; match ← TRUE};
	ENDCASE;
      IF entry.type ~= DEfree AND dir.spaceFound < dir.spaceNeeded THEN
	dir.spaceFound ← 0;
      RETURN[match]
      END;

    CheckFP: PROCEDURE [entry: DVPtr, entryName: STRING, entryPos: Position]
      RETURNS [BOOLEAN] =
      -- matches a file name entry against fp↑.  A match terminates the enumeration.
      -- As a side effect, this procedure records the matching string name in 'name'.
      BEGIN
      match: BOOLEAN ← entry.type = DEfile AND fp↑ = FP[
	entry.fp.serial, entry.fp.leaderDA];
      IF match THEN
	BEGIN
	length: CARDINAL ← MIN[entryName.length, name.maxlength];
	FOR i: CARDINAL IN [0..length) DO name[i] ← entryName[i]; ENDLOOP;
	name.length ← length;
	pos ← entryPos;
	END;
      RETURN[match]
      END;

    SELECT type FROM
      name =>
	BEGIN
	dir.spaceFound ← 0;
	dir.spaceNeeded ← SIZE[DV] + StringDefs.WordsForBcplString[name.length];
	found ← DoEnumerate[dir, CheckName];
	IF dir.spaceFound = 0 THEN dir.spacePos ← dir.length;
	END;
      fp => found ← DoEnumerate[dir, CheckFP];
      ENDCASE;
    END;

  DoDelete: ENTRY PROCEDURE [
    dir: DirHandle, type: LookupType, fullName: STRING, fp: FPPtr]
    RETURNS [found: BOOLEAN] =
    -- attempts to find and delete an entry in 'dir'.
    BEGIN
    ENABLE UNWIND => NULL;
    pos: Position;
    [found, pos] ← DoLookup[dir, type, fullName, fp];
    IF found THEN DeleteEntry[dir, pos];
    END;

  InsertEntry: PROCEDURE [dir: DirHandle, name: STRING, fp: FPPtr] =
    -- inserts the entry <name, fp> into directory 'dir'.  It is assumed that the
    -- space-related fields of 'dir' are valid.
    BEGIN
    nameBuffer: ARRAY [0..bcplNameSize) OF UNSPECIFIED;
    bcplFileName: POINTER TO StringDefs.BcplSTRING = LOOPHOLE[@nameBuffer];
    entry: DV;
    pos: Position ← dir.spacePos;
    leftOver: CARDINAL;
    IF dir.spaceFound < dir.spaceNeeded THEN -- extension will be needed
      BEGIN
      EnsureDirectoryLength[dir,
        IncrementPosition[dir.spacePos, dir.spaceNeeded*AltoDefs.BytesPerWord]];
      dir.spaceFound ← dir.spaceNeeded;
      END;
    entry ← DV[DEfile, dir.spaceNeeded, CFP[fp.serial, 1, 0, fp.leaderDA]];
    Write[dir, @pos, @entry, SIZE[DV]];
    StringDefs.MesaToBcplString[name, bcplFileName];
    IF bcplFileName.length MOD 2 = 0 THEN -- keep Boggs happy
      bcplFileName.char[bcplFileName.length] ← 0C;
    Write[dir, @pos, bcplFileName, dir.spaceNeeded - SIZE[DV]];
    IF (leftOver ← dir.spaceFound - dir.spaceNeeded) > 0 THEN
      {entry ← DV[DEfree, leftOver, ]; Write[dir, @pos, @entry, 1]};
    VMDefs.Start[dir.buffer];
    VMDefs.WaitFile[dir.file];
    END;

  DeleteEntry: PROCEDURE [dir: DirHandle, pos: Position] = -- INLINE --
    -- removes the entry beginning at address 'pos' in directory 'dir'.
    BEGIN
    entry: DV;
    oldPos: Position ← pos;
    Read[dir, @pos, @entry, 1]; -- read old entry to get size
    entry.type ← DEfree;
    Write[dir, @oldPos, @entry, 1];
    VMDefs.Start[dir.buffer];
    VMDefs.WaitFile[dir.file];
    END;

  -- Enumeration Procedure --

  DoEnumerate: PROCEDURE [
    dir: DirHandle,
    proc: PROCEDURE [DVPtr, STRING, Position] RETURNS [BOOLEAN]]
    RETURNS [found: BOOLEAN] =
    -- scans directory 'dir' calling 'proc' for each item.  The DVPtr points to the
    -- fixed information, the STRING is valid only when entry.type = DEfile.  The
    -- Position indicates the location of the entry in 'dir'.
    BEGIN
    bcplFileName: ARRAY [0..bcplNameSize) OF UNSPECIFIED;
    fileName: STRING ← [fileNameChars];
    entry: DV;
    entryLength: CARDINAL;
    pos: Position ← [0, 0];

    FillEntry: PROCEDURE = INLINE
      -- reads the directory entry beginning at 'pos' into 'entry' and 'bcplName'.
      BEGIN
      tempPos: Position ← pos;
      Read[dir, @tempPos, @entry, 1];
      IF (entryLength ← entry.length) = 0 THEN ERROR BadDirectory;
      fileName.length ← 0;
      IF entry.type = DEfile THEN
	IF entryLength IN (SIZE[DV]..SIZE[DV] + LENGTH[bcplFileName]] THEN
	  BEGIN
	  Read[dir, @tempPos, @entry + 1, SIZE[DV] - 1];
	  Read[dir, @tempPos, @bcplFileName, entryLength - SIZE[DV]];
	  StringDefs.BcplToMesaString[LOOPHOLE[@bcplFileName], fileName];
	  END
	ELSE entry.type ← DEugly;
      END;

    DO
      IF pos = dir.length THEN RETURN[FALSE];
      FillEntry[];
      IF (found ← proc[@entry, fileName, pos]) THEN RETURN;
      pos ← IncrementPosition[pos, entryLength*AltoDefs.BytesPerWord];
      ENDLOOP;
    END;

  -- Quick-and-Dirty "Streaming" Procedures --

  Read: PROCEDURE [
    dir: DirHandle, pos: POINTER TO Position, p: POINTER, n: [0..AltoDefs.PageSize)] =
    -- reads 'n' words from <file, pos> into the buffer beginning at 'p'.
    BEGIN OPEN AltoDefs, Inline;
    base: POINTER;
    wordsLeft: CARDINAL;
    EnsureProperBuffer[dir, pos.page];
    base ← dir.buffer + (pos.byte/BytesPerWord);
    wordsLeft ← PageSize - (pos.byte/BytesPerWord);
    IF wordsLeft >= n THEN COPY[to: p, from: base, nwords: n]
    ELSE
      BEGIN -- block straddles a page boundary
      IF wordsLeft > 0 THEN COPY[to: p, from: base, nwords: wordsLeft];
      EnsureProperBuffer[dir, pos.page + 1];
      base ← LOOPHOLE[dir.buffer];
      COPY[to: p + wordsLeft, from: base, nwords: n - wordsLeft];
      END;
    pos↑ ← IncrementPosition[pos↑, n*AltoDefs.BytesPerWord];
    END;

  Write: PROCEDURE [
    dir: DirHandle, pos: POINTER TO Position, p: POINTER, n: [0..AltoDefs.PageSize)] =
    -- writes 'n' words from the buffer beginning at 'p' to <file, pos>.  It is assumed
    -- that the backing file is long enough to accommodate the write, i.e., no extension
    -- is performed.
    BEGIN
    OPEN VMDefs;
    base: POINTER;
    wordsLeftOnPage: CARDINAL = AltoDefs.PageSize - (pos.byte/AltoDefs.BytesPerWord);
    newPos: Position = IncrementPosition[pos↑, n*AltoDefs.BytesPerWord];
    EnsureProperBuffer[dir, pos.page];
    base ← dir.buffer + (pos.byte/AltoDefs.BytesPerWord);
    IF wordsLeftOnPage >= n THEN Inline.COPY[to: base, from: p, nwords: n]
    ELSE
      BEGIN
      -- block straddles a page boundary.  Assert:  0 < wordsLeftOnPage < n
      Inline.COPY[to: base, from: p, nwords: wordsLeftOnPage];
      MarkStart[dir.buffer];
      EnsureProperBuffer[dir, pos.page + 1];
      base ← dir.buffer;
      Inline.COPY[to: base, from: p + wordsLeftOnPage, nwords: n - wordsLeftOnPage];
      END;
    Mark[dir.buffer];
    pos↑ ← newPos;
    END;

  EnsureDirectoryLength: PROCEDURE [dir: DirHandle, newEnd: Position] =
    -- ensures that the length of the backing file dir.file spans 'newEnd'.
    BEGIN
    IF ComparePositions[newEnd, dir.length] = greater THEN
      BEGIN
      VMDefs.SetFileLength[dir.file, [newEnd.page, newEnd.byte]];
      ResetDirectoryLength[dir];
      END;
    END;

  EnsureProperBuffer: PROCEDURE [dir: DirHandle, page: VMDefs.PageNumber] =
    -- ensures that <dir.file, page> is associated with 'dir'.
    BEGIN OPEN VMDefs;
    IF dir.page ~= page THEN
      BEGIN
      IF dir.buffer ~= NIL THEN Release[dir.buffer];
      dir.page ← page;
      dir.buffer ← ReadPage[[dir.file, page], 2];
      END;
    END;

  -- File Name Manipulation --

  ValidateAndExpandName: PROCEDURE [fullName, name: STRING] =
    -- ensures that the file name contains no illegal characters and
    -- terminates with a '.'.
    BEGIN OPEN StringDefs;
    ENABLE StringBoundsFault => GO TO nameTooLong;
    char: CHARACTER ← 0C;
    FOR i: CARDINAL IN [0..name.length) DO
      SELECT (char ← name[i]) FROM
	IN ['a..'z], IN ['A..'Z], IN ['0..'9], '., '$, '!, '?, '+, '-, '<, '> =>
	  AppendChar[fullName, char];
	ENDCASE => ERROR IllegalFileName;
      ENDLOOP;
    IF char ~= '. THEN AppendChar[fullName, '.];
    EXITS
      nameTooLong => ERROR IllegalFileName;
    END;

  END.