-- Directory.Mesa  Edited by Sandman on July 9, 1980  4:13 PM
-- Copyright  Xerox Corporation 1979, 1980

DIRECTORY
  AltoDefs USING [BytesPerWord],
  AltoFileDefs USING [DEfile, DEfree, DirFP, DV, FA, FilenameChars, FP],
  BFSDefs USING [CreateFile, MakeCFP, MakeFP],
  DirectoryDefs USING [DEptr],
  InlineDefs USING [BITAND, COPY],
  NucleusOps USING [],
  SegmentDefs USING [
    AccessOptions, FileHandle, InsertFile, InsufficientVM, LockFile, NewFile,
    OldFileOnly, ReleaseFile, UnlockFile],
  StreamDefs USING [
    AccessOptions, Append, CreateWordStream, JumpToFA, Read, ReadBlock,
    SetPosition, DiskHandle, Write, WriteBlock],
  StreamScan USING [Finish, GetBuffer, Init, Handle],
  StringDefs USING [
    AppendChar, BcplSTRING, BcplToMesaString, MesaToBcplString,
    WordsForBcplString],
  Storage USING [FreePages, Pages];

Directory: PROGRAM
  IMPORTS
    BFSDefs, InlineDefs, SegmentDefs, StreamDefs, StreamScan, StringDefs, Storage
  EXPORTS DirectoryDefs, NucleusOps =PUBLIC

  BEGIN OPEN AltoFileDefs, StreamDefs, StringDefs;

  DEptr: PRIVATE TYPE = DirectoryDefs.DEptr;
  FPptr: TYPE = POINTER TO AltoFileDefs.FP;
  HDptr: TYPE = POINTER TO HD;

  BadFilename: SIGNAL [name: STRING] = CODE;
  BadDirectory: SIGNAL = CODE;

  Enumerate: PROCEDURE [
    dir: DiskHandle, proc: PROCEDURE [FPptr, STRING] RETURNS [BOOLEAN]] =
    BEGIN
    false: BOOLEAN ← FALSE;

    PassItOn: PROCEDURE [i: CARDINAL, ssd: StreamScan.Handle, de: DEptr]
      RETURNS [BOOLEAN] =
      BEGIN
      s: STRING ← [AltoFileDefs.FilenameChars];
      fp: AltoFileDefs.FP;
      BFSDefs.MakeFP[@fp, @de.dv.fp];
      BcplToMesaString[@de.name, s];
      RETURN[proc[@fp, s]]
      END;

      [] ← EnumerateEntries[dir, PassItOn, @false];
    RETURN
    END;

  EnumerateDirectory: PROCEDURE [
    proc: PROCEDURE [FPptr, STRING] RETURNS [BOOLEAN]] =
    BEGIN
    dir: DiskHandle ← CreateWordStream[workingDir, Read];
    Enumerate[dir, proc];
    dir.destroy[dir];
    RETURN
    END;

  Insert: PROCEDURE [dir: DiskHandle, fp: FPptr, name: STRING]
    RETURNS [old: BOOLEAN] =
    BEGIN
    hd: HD;
    fn: STRING = [AltoFileDefs.FilenameChars];
    tFP: AltoFileDefs.FP;
    MakeLegalFileName[name, fn, 0, name.length];
    old ← FindName[dir, @tFP, fn, @hd].found;
    IF old THEN RETURN;
    MakeEntry[dir, fp, name, @hd];
    RETURN
    END;

  Lookup: PROCEDURE [dir: DiskHandle, fp: FPptr, name: STRING, create: BOOLEAN]
    RETURNS [old: BOOLEAN] =
    BEGIN
    hd: HD;
    fn: STRING = [AltoFileDefs.FilenameChars];
    MakeLegalFileName[name, fn, 0, name.length];
    old ← FindName[dir, fp, fn, @hd].found;
    IF ~old AND create THEN
      BEGIN
      BFSDefs.CreateFile[fn, fp, @dir.file.fp];
      MakeEntry[dir, fp, fn, @hd];
      END;
    RETURN
    END;

  DirectoryLookup: PROCEDURE [fp: FPptr, name: STRING, create: BOOLEAN]
    RETURNS [old: BOOLEAN] =
    BEGIN
    fn: STRING = [AltoFileDefs.FilenameChars];
    hd: HD;
    access: AccessOptions = IF ~create THEN Read ELSE Read + Write + Append;
    dir: DiskHandle = ParseFileName[name, fn, access];
    old ← FindName[dir, fp, fn, @hd].found;
    IF ~old AND create THEN
      BEGIN
      BFSDefs.CreateFile[fn, fp, @dir.file.fp];
      MakeEntry[dir, fp, fn, @hd];
      END;
    dir.destroy[dir];
    RETURN
    END;

  LookupFP: PROCEDURE [dir: DiskHandle, fp: FPptr, name: STRING]
    RETURNS [old: BOOLEAN] = BEGIN RETURN[FindFP[dir, fp, name].found]; END;

  DirectoryLookupFP: PROCEDURE [fp: FPptr, name: STRING] RETURNS [old: BOOLEAN] =
    BEGIN
    dir: DiskHandle = CreateWordStream[workingDir, Read];
    old ← FindFP[dir, fp, name].found;
    dir.destroy[dir];
    RETURN
    END;

  Purge: PROCEDURE [dir: DiskHandle, fp: FPptr, name: STRING]
    RETURNS [found: BOOLEAN] =
    BEGIN
    index: CARDINAL;
    fn: STRING = [AltoFileDefs.FilenameChars];
    MakeLegalFileName[name, fn, 0, name.length];
    [found, index] ← FindName[dir, fp, fn, NIL];
    IF found THEN DeleteEntry[dir, index];
    RETURN
    END;

  DirectoryPurge: PROCEDURE [fp: FPptr, name: STRING] RETURNS [found: BOOLEAN] =
    BEGIN
    index: CARDINAL;
    fn: STRING = [AltoFileDefs.FilenameChars];
    dir: DiskHandle ← ParseFileName[name, fn, Read + Write];
    [found, index] ← FindName[dir, fp, fn, NIL];
    IF found THEN DeleteEntry[dir, index];
    dir.destroy[dir];
    RETURN
    END;

  PurgeFP: PROCEDURE [dir: DiskHandle, fp: FPptr] RETURNS [found: BOOLEAN] =
    BEGIN
    index: CARDINAL;
    [found, index] ← FindFP[dir, fp, NIL];
    IF found THEN DeleteEntry[dir, index];
    RETURN
    END;

  DirectoryPurgeFP: PROCEDURE [fp: FPptr] RETURNS [found: BOOLEAN] =
    BEGIN
    dir: DiskHandle ← CreateWordStream[workingDir, Read + Write];
    found ← PurgeFP[dir, fp];
    dir.destroy[dir];
    RETURN
    END;

  -- Support Routines

  DEugly: INTEGER = AltoFileDefs.DEfile + 1; -- for malformed DEfile entries.

  BcplWords: INTEGER = 20; -- WordsForBcplString[FilenameChars];

  HD: TYPE = RECORD [size, needed, index: CARDINAL];

  maxBuffers: CARDINAL = 2;

  upMask: CARDINAL = 137B; -- masks lower case to upper case (sort of)


  EnumerateEntries: PROCEDURE [
    dir: DiskHandle,
    proc: PROCEDURE [CARDINAL, StreamScan.Handle, DEptr] RETURNS [BOOLEAN],
    inspectFree: POINTER TO READONLY BOOLEAN, lengthFilter: CARDINAL ← 0]
    RETURNS [index: CARDINAL] =
    BEGIN OPEN AltoFileDefs;
    minLength: CARDINAL =
      IF lengthFilter = 0 THEN 0 ELSE SIZE[AltoFileDefs.DV] + lengthFilter/2 + 1;
    buffer: POINTER;
    pos, endPos: CARDINAL ← 0;
    deSpace: ARRAY [0..SIZE[DV] + BcplWords) OF UNSPECIFIED;
    tempDE: DEptr = LOOPHOLE[@deSpace];
    ssd: StreamScan.Handle;
    bufArray: ARRAY [0..maxBuffers) OF POINTER;
    nBufs: CARDINAL;

    AdvanceBuffer: PROCEDURE =
      BEGIN
      buffer ← StreamScan.GetBuffer[ssd];
      index ← index + endPos;
      pos ← pos - endPos;
      endPos ← ssd.numChars/AltoDefs.BytesPerWord
      END;

    -- allocate buffers for scanning
    FOR nBufs ← 0, nBufs + 1 UNTIL nBufs = maxBuffers DO
      bufArray[nBufs] ← Storage.Pages[
	1 ! SegmentDefs.InsufficientVM => IF nBufs # 0 THEN EXIT];
      ENDLOOP;
    dir.reset[dir];
    index ← 0;
    ssd ← StreamScan.Init[dir, @bufArray, nBufs];
    AdvanceBuffer[]; -- now search until eof
    UNTIL buffer = NIL DO
      BEGIN
      thisDE: DEptr ← buffer + pos;
      IF thisDE.dv.length = 0 THEN ERROR BadDirectory;
      SELECT thisDE.dv.type FROM
	DEfile =>
	  IF thisDE.dv.length >= minLength THEN
	    BEGIN -- this might be an interesting entry
	    thisIndex: CARDINAL = index + pos;
	    length: CARDINAL;
	    pos ← pos + (length ← thisDE.dv.length);
	    IF pos > endPos THEN -- overflow
	      BEGIN OPEN InlineDefs;
	      COPY[from: thisDE, to: tempDE, nwords: length - (pos - endPos)];
	      AdvanceBuffer[];
	      COPY[from: buffer, to: tempDE + length - pos, nwords: pos];
	      thisDE ← tempDE;
	      END;
	    IF (thisDE.name.length = lengthFilter OR lengthFilter = 0) AND proc[
	      thisIndex, ssd, thisDE] THEN BEGIN index ← thisIndex; EXIT END;
	    END
	  ELSE pos ← pos + thisDE.dv.length;
	DEfree =>
	  BEGIN
	  thisIndex: CARDINAL ← index + pos;
	  IF inspectFree↑ AND proc[thisIndex, ssd, thisDE] THEN
	    BEGIN index ← thisIndex; EXIT END;
	  pos ← pos + thisDE.dv.length;
	  END;
	ENDCASE => pos ← pos + thisDE.dv.length;
      WHILE pos >= endPos AND buffer # NIL DO AdvanceBuffer[]; ENDLOOP;
      END;
      ENDLOOP; -- finish the scan and free the buffers
    StreamScan.Finish[ssd];
    BEGIN
    i: CARDINAL;
    FOR i IN [0..nBufs) DO Storage.FreePages[bufArray[i]]; ENDLOOP;
    END;
    RETURN
    END;

  FindName: PRIVATE PROCEDURE [
    dir: DiskHandle, fp: FPptr, name: STRING, hd: HDptr]
    RETURNS [found: BOOLEAN, index: CARDINAL] =
    BEGIN OPEN AltoFileDefs;
    inspectFree: BOOLEAN ← TRUE;
    holeFA: FA;

    MatchName: PROCEDURE [i: CARDINAL, ssd: StreamScan.Handle, de: DEptr]
      RETURNS [BOOLEAN] =
      BEGIN
      SELECT de.dv.type FROM
	DEfile =>
	  BEGIN
	  n: STRING = capitalizedName;
	  i: CARDINAL;
	  FOR i IN [0..n.length) DO
	    IF n[i] # InlineDefs.BITAND[de.name.char[i], upMask] THEN
	      RETURN[FALSE];
	    ENDLOOP;
	  BFSDefs.MakeFP[fp, @de.dv.fp];
	  found ← TRUE;
	  END;
	DEfree =>
	  BEGIN -- executed only if inspectFree (and therefore hd#NIL)
	  IF hd.index + hd.size # i OR i = 0 THEN -- not adjacent to previous hole
	    BEGIN
	    hd.index ← i;
	    hd.size ← 0;
	    holeFA ← [da: ssd.da, page: ssd.pageNumber, byte: 0];
	    END;
	  hd.size ← hd.size + de.dv.length;
	  IF hd.size >= hd.needed THEN inspectFree ← FALSE;
	  RETURN[FALSE]
	  END;
	ENDCASE; -- SIGNAL BadDirectory;
      RETURN[found]
      END;

    -- Note: the following is not a true capitalization algorithm, but
    -- it maps non-alphabetics into unique characters that are not
    -- legal in Alto file names.
    i: CARDINAL;
    capitalizedName: STRING = [FilenameChars];
    capitalizedName.length ← name.length;
    FOR i IN [0..name.length) DO
      capitalizedName[i] ← InlineDefs.BITAND[name[i], upMask]; ENDLOOP;
    IF hd = NIL THEN inspectFree ← FALSE
    ELSE hd↑ ← HD[0, SIZE[DV] + WordsForBcplString[name.length], 0];
    found ← FALSE;
    index ← EnumerateEntries[dir, MatchName, @inspectFree, name.length];
    IF inspectFree THEN hd.index ← index
    ELSE IF ~found THEN JumpToFA[dir, @holeFA];
    RETURN
    END;

  FindFP: PRIVATE PROCEDURE [dir: DiskHandle, fp: FPptr, name: STRING]
    RETURNS [found: BOOLEAN, index: CARDINAL] =
    BEGIN

    MatchFP: PROCEDURE [i: CARDINAL, ssd: StreamScan.Handle, de: DEptr]
      RETURNS [BOOLEAN] =
      BEGIN -- de is guaranteed to be type DEFile
      f: AltoFileDefs.FP;
      BFSDefs.MakeFP[@f, @de.dv.fp];
      IF (found ← f = fp↑) AND name # NIL THEN
	BEGIN name.length ← 0; BcplToMesaString[@de.name, name]; END;
      RETURN[found]
      END;

    found ← FALSE;
    index ← EnumerateEntries[dir, MatchFP, @found];
    RETURN
    END;

  MakeEntry: PRIVATE PROCEDURE [
    dir: DiskHandle, fp: FPptr, name: STRING, hd: HDptr] =
    BEGIN OPEN AltoFileDefs;
    leftover: CARDINAL;
    dv: DV ← DV[DEfile, hd.needed, ];
    dn: ARRAY [0..BcplWords) OF UNSPECIFIED;
    bcpl: POINTER TO BcplSTRING ← @dn[0];
    IF name.length > AltoFileDefs.FilenameChars THEN ERROR BadFilename[name];
    BFSDefs.MakeCFP[@dv.fp, fp];
    MesaToBcplString[name, bcpl];
    IF bcpl.length MOD 2 = 0 THEN bcpl.char[bcpl.length] ← 0C;
    SetPosition[dir, 2*hd.index];
    [] ← WriteBlock[dir, @dv, SIZE[DV]];
    [] ← WriteBlock[dir, bcpl, hd.needed - SIZE[DV]];
    IF (leftover ← hd.size - hd.needed) > 0 THEN
      BEGIN dv ← DV[DEfree, leftover, ]; [] ← WriteBlock[dir, @dv, 1]; END;
    RETURN
    END;

  DeleteEntry: PRIVATE PROCEDURE [dir: DiskHandle, index: CARDINAL] =
    BEGIN
    dv: AltoFileDefs.DV;
    SetPosition[dir, 2*index];
    [] ← ReadBlock[dir, @dv, 1];
    dv.type ← AltoFileDefs.DEfree;
    SetPosition[dir, 2*index];
    [] ← WriteBlock[dir, @dv, 1];
    RETURN
    END;

  -- Filename Parsing


  ParseFileName: PROCEDURE [
    name, filename: STRING, dirAccess: SegmentDefs.AccessOptions]
    RETURNS [StreamDefs.DiskHandle] =
    BEGIN OPEN SegmentDefs;
    dirFile: FileHandle;
    dirName: STRING = [100];
    i: CARDINAL;
    sep: CARDINAL ← 0;
    filename.length ← 0;
    FOR i DECREASING IN [0..name.length) DO
      IF name[i] = '< OR name[i] = '> THEN
	BEGIN
	sep ← i;
	MakeLegalFileName[name, dirName, 0, sep];
	MakeLegalFileName[name, filename, sep + 1, name.length];
	EXIT
	END;
      REPEAT FINISHED => MakeLegalFileName[name, filename, 0, name.length];
      ENDLOOP;
    IF dirName.length = 0 THEN
      BEGIN IF name[sep] = '< THEN dirFile ← sysDir ELSE dirFile ← workingDir; END
    ELSE dirFile ← NewFile[dirName, dirAccess, OldFileOnly];
    RETURN[StreamDefs.CreateWordStream[dirFile, dirAccess]]
    END;

  MakeLegalFileName: PRIVATE PROCEDURE [
    src, dest: STRING, first, last: CARDINAL] =
    BEGIN
    i: CARDINAL;
    char: CHARACTER;
    FOR i IN [first..last) DO
      char ← src[i];
      SELECT char FROM
	IN ['a..'z], IN ['A..'Z], IN ['0..'9], '., '$, '!, '?, '+, '-, '<, '> =>
	  StringDefs.AppendChar[dest, char];
	ENDCASE => StringDefs.AppendChar[dest, '-];
      ENDLOOP;
    IF dest.length = 0 OR dest[dest.length - 1] # '. THEN
      StringDefs.AppendChar[dest, '.];
    RETURN
    END;

  SetWorkingDir: PROCEDURE [dir: SegmentDefs.FileHandle] =
    BEGIN OPEN SegmentDefs;
    IF dir # NIL AND dir.fp.serial.bits.directory = 0 THEN ERROR BadDirectory;
    UnlockFile[workingDir];
    IF workingDir.segcount = 0 THEN ReleaseFile[workingDir];
    workingDir ← IF dir = NIL THEN sysDir ELSE dir;
    LockFile[workingDir];
    END;

  GetWorkingDir: PROCEDURE RETURNS [SegmentDefs.FileHandle] =
    BEGIN RETURN[workingDir] END;

  init: PRIVATE PROCEDURE =
    BEGIN OPEN SegmentDefs;
    fp: AltoFileDefs.FP ← AltoFileDefs.DirFP;
    sysDir ← workingDir ← InsertFile[@fp, Read + Write + Append];
    LockFile[sysDir];
    LockFile[workingDir];
    END;

  -- Main Body

  sysDir, workingDir: SegmentDefs.FileHandle;

  init[];

  END.