-- FileStreamImpl.mesa, edited by Johnsson, 13-Feb-81 14:14:14

DIRECTORY
  ByteBlt USING [ByteBlt],
  DCSFileTypes USING [tLeaderPage],
  Environment USING [
    Block, Byte, bytesPerPage, PageCount, wordsPerPage],
  File USING [
    Capability, GetAttributes, GetSize, grow, PageCount, Permissions,
    read, SetSize, shrink, write],
  FileStream USING [Subtype],
  Heap USING [systemMDSZone],
  Inline USING [
    BITAND, BITOR, HighHalf, LongCOPY, LongDiv, LongDivMod, LongMult, LowHalf],
  Space USING [
    CopyIn, Create, CreateUniformSwapUnits, Delete, Error, ForceOut, GetAttributes,
    GetHandle, GetWindow, Handle, LongPointer, Map, PageFromLongPointer,
    Unmap, virtualMemory],
  Stream USING [
    Block, Byte, defaultInputOptions, EndOfStream, GetProcedure,
    Handle, InputOptions, Object, PutProcedure, SendAttentionProcedure,
    SetSSTProcedure, ShortBlock, WaitAttentionProcedure],
  System USING [GetGreenwichMeanTime, GreenwichMeanTime];

FileStreamImpl: PROGRAM
  IMPORTS ByteBlt, File, Heap, Inline, Space, Stream, System
  EXPORTS FileStream
  SHARES File =
  BEGIN

  bytesPerPage: CARDINAL = Environment.bytesPerPage;

  FSObject: TYPE = RECORD [
    stream: Stream.Object,
    index: CARDINAL ← 0,
    bufferBytes: CARDINAL ← 0,
    dataBytesInBuffer: CARDINAL ← 0,
    firstFilePageInBuffer: CARDINAL ← 0,
    filePages: CARDINAL ← 0,
    password: CARDINAL ← fsPassword,
    buffer: LONG POINTER TO PACKED ARRAY [0..0) OF Environment.Byte ← NIL,
    leader: LONG POINTER TO LeaderPage ← NIL,
    eofInBuffer: BOOLEAN ← FALSE,
    read: BOOLEAN,
    write: BOOLEAN,
    grow: BOOLEAN,
    shrink: BOOLEAN,
    lengthChanged: BOOLEAN ← FALSE,
    dataOffset: [0..1] ← 0,
    file: File.Capability,
    bufferSpace: Space.Handle ← NULL];

  fsPassword: CARDINAL = 102774B;

  FSHandle: TYPE = POINTER TO FSObject;

  conversionFudge: CARDINAL = LOOPHOLE[@LOOPHOLE[0,FSHandle].stream];

  ConvertHandle: PROCEDURE [h: Stream.Handle] RETURNS [FSHandle] =
    INLINE {RETURN[LOOPHOLE[h-conversionFudge]]};

  InvalidHandle: PUBLIC ERROR [errorStream: Stream.Handle] = CODE;

  InvalidOperation: PUBLIC ERROR [errorStream: Stream.Handle] = CODE;

  ValidateHandle: PROCEDURE [h: Stream.Handle] RETURNS [FSHandle] =
    INLINE BEGIN
    t: FSHandle ← ConvertHandle[h];
    IF t.password # fsPassword THEN ERROR InvalidHandle[h];
    RETURN[t];
    END;

  FileTooLong: PUBLIC ERROR [longFile: POINTER TO File.Capability] = CODE;
  
  Create: PUBLIC PROCEDURE [
    capability: File.Capability, options: Stream.InputOptions ← Stream.defaultInputOptions]
    RETURNS [Stream.Handle] =
    BEGIN
    fsh: FSHandle = Heap.systemMDSZone.NEW[FSObject];
    permissions: File.Permissions = capability.permissions;
    filePages: File.PageCount = File.GetSize[capability];
    IF Inline.HighHalf[filePages] # 0 THEN ERROR FileTooLong[@capability];
    capability.permissions ← Inline.BITOR[permissions, File.read];
    fsh↑ ← [
      stream: [
        getByte: GetByte,
        putByte: PutByte,
        getWord: GetWord,
        putWord: PutWord,
        get: GetBlock,
        put: PutBlock,
        delete: Delete,
        setSST: SetSSTNop,
        sendAttention: SendAttentionNop,
        waitAttention: WaitAttentionNop,
        options: options],
      read: Inline.BITAND[permissions, File.read] # 0,
      write: Inline.BITAND[permissions, File.write] # 0,
      grow: Inline.BITAND[permissions, File.grow] # 0,
      shrink: Inline.BITAND[permissions, File.shrink] # 0,
      filePages: Inline.LowHalf[filePages],
      file: capability];
    IF ~fsh.read THEN
      BEGIN
      fsh.stream.getByte ← fsh.stream.getWord ← GetError;
      fsh.stream.get ← GetBlockError;
      END;
    IF ~fsh.write THEN
      BEGIN
      fsh.stream.putByte ← LOOPHOLE[PutError];
      fsh.stream.putWord ← PutError;
      fsh.stream.put ← PutBlockError;
      END;
    InitLeader[fsh];
    SetupBuffer[fsh, fsh.dataOffset];
    RETURN[@fsh.stream];
    END;

  -- Generic Stream procedures; assume handle ok

  GetByte: PROCEDURE [sH: Stream.Handle] RETURNS [byte: UNSPECIFIED[0..256)] =
    BEGIN
    fsh: FSHandle ← ConvertHandle[sH];
    WHILE fsh.index = fsh.dataBytesInBuffer DO
      IF fsh.eofInBuffer THEN {SIGNAL Stream.EndOfStream[0]; RETURN[0]};
      AdvanceBuffer[fsh];
      ENDLOOP;
    byte ← fsh.buffer[fsh.index];
    fsh.index ← fsh.index + 1;
    RETURN
    END;

  GetWord: PROCEDURE [sH: Stream.Handle] RETURNS [UNSPECIFIED] =
    BEGIN
    t: UNSPECIFIED = CARDINAL[GetByte[sH]] * 256;
    RETURN[t + GetByte[sH]]
    END;

  GetError: PROCEDURE [sH: Stream.Handle] RETURNS [UNSPECIFIED] =
    {ERROR InvalidOperation[sH]};

  PutByte: PROCEDURE [sH: Stream.Handle, byte: UNSPECIFIED[0..256)] =
    BEGIN
    fsh: FSHandle ← ConvertHandle[sH];
    WHILE fsh.index = fsh.dataBytesInBuffer DO
      IF fsh.eofInBuffer AND ~fsh.grow THEN {Stream.EndOfStream[0]; RETURN};
      IF fsh.dataBytesInBuffer = fsh.bufferBytes THEN AdvanceBuffer[fsh]
      ELSE
        BEGIN
        fsh.dataBytesInBuffer ← fsh.dataBytesInBuffer + 1;
        fsh.lengthChanged ← TRUE;
        END;
      ENDLOOP;
    fsh.buffer[fsh.index] ← byte;
    fsh.index ← fsh.index + 1;
    RETURN
    END;

  PutWord: PROCEDURE [sH: Stream.Handle, word: UNSPECIFIED] =
    BEGIN
    PutByte[sH, CARDINAL[word] / 256];
    PutByte[sH, CARDINAL[word] MOD 256];
    RETURN
    END;

  PutError: PROCEDURE [sH: Stream.Handle, word: UNSPECIFIED] =
    {ERROR InvalidOperation[sH]};

  GetBlock: Stream.GetProcedure =
    BEGIN
    fsh: FSHandle = ConvertHandle[sH];
    bufferBlock: Environment.Block;
    countRemaining: CARDINAL ← block.stopIndexPlusOne-block.startIndex;
    countTransferred: CARDINAL;
    sst ← 0;
    bytesTransferred ← 0;
    why ← normal;
    WHILE countRemaining # 0 DO
      bufferBlock ← [
        blockPointer: fsh.buffer,
        startIndex: fsh.index,
        stopIndexPlusOne: fsh.dataBytesInBuffer];
      countTransferred ← ByteBlt.ByteBlt[from: bufferBlock, to: block];
      fsh.index ← fsh.index + countTransferred;
      bytesTransferred ← bytesTransferred + countTransferred;
      IF (countRemaining ← countRemaining - countTransferred) = 0 THEN EXIT;
      IF fsh.eofInBuffer THEN
        BEGIN
        IF options.signalEndOfStream THEN
          SIGNAL Stream.EndOfStream[block.startIndex+countTransferred];
        IF options.signalShortBlock THEN ERROR Stream.ShortBlock;
        why ← endOfStream;
        EXIT
        END;
      block.startIndex ← block.startIndex + countTransferred;
      AdvanceBuffer[fsh];
      ENDLOOP;
    RETURN
    END;

  GetBlockError: Stream.GetProcedure = {ERROR InvalidOperation[sH]};

  PutBlock: Stream.PutProcedure =
    BEGIN
    fsh: FSHandle = ConvertHandle[sH];
    bufferBlock: Environment.Block;
    countRemaining: CARDINAL ← block.stopIndexPlusOne-block.startIndex;
    countTransferred: CARDINAL;
    WHILE countRemaining # 0 DO
      bufferBlock ← [
        blockPointer: fsh.buffer,
        startIndex: fsh.index,
        stopIndexPlusOne: fsh.dataBytesInBuffer];
      IF fsh.eofInBuffer AND fsh.grow THEN
        bufferBlock.stopIndexPlusOne ← fsh.bufferBytes;
      countTransferred ← ByteBlt.ByteBlt[from: block, to: bufferBlock];
      fsh.index ← fsh.index + countTransferred;
      IF fsh.eofInBuffer AND fsh.index > fsh.dataBytesInBuffer THEN {
	fsh.dataBytesInBuffer ← fsh.index; fsh.lengthChanged ← TRUE};
      IF (countRemaining ← countRemaining - countTransferred) = 0 THEN EXIT;
      IF fsh.eofInBuffer THEN
        IF ~fsh.grow THEN {
          IF fsh.stream.options.signalEndOfStream THEN
            SIGNAL Stream.EndOfStream[block.startIndex+countTransferred];
          IF fsh.stream.options.signalShortBlock THEN ERROR Stream.ShortBlock;
          EXIT}
        ELSE {
          fsh.dataBytesInBuffer ← fsh.bufferBytes;
          fsh.lengthChanged ← TRUE};
      block.startIndex ← block.startIndex + countTransferred;
      AdvanceBuffer[fsh];
      ENDLOOP;
    IF endPhysicalRecord THEN Cleanup[fsh];
    RETURN
    END;

  PutBlockError: Stream.PutProcedure = {ERROR InvalidOperation[sH]};

  Delete: PROCEDURE [sH: Stream.Handle] =
    BEGIN
    fsh: FSHandle ← ConvertHandle[sH];
    newFilePages: CARDINAL;
    index: LONG CARDINAL ← GetIndex[@fsh.stream];
    ReadLeader[fsh];
    EmptyBuffer[fsh, TRUE];
    IF ~fsh.read AND index # 0 THEN fsh.leader.length ← index;
    newFilePages ← Inline.LowHalf[PagesForBytes[fsh.leader.length]] + fsh.dataOffset;
    IF fsh.write AND newFilePages < fsh.filePages AND fsh.shrink THEN
      File.SetSize[fsh.file, newFilePages];
    CloseLeader[fsh];
    Zero[fsh, SIZE[FSObject]];
    Heap.systemMDSZone.FREE[@fsh];
    RETURN
    END;

  SetSSTNop: Stream.SetSSTProcedure = {};

  SendAttentionNop: Stream.SendAttentionProcedure = {};

  WaitAttentionNop: Stream.WaitAttentionProcedure = {RETURN[0]};

  -- FileStream specific procedures; must validate handle

  EndOf: PUBLIC PROCEDURE [sH: Stream.Handle] RETURNS [BOOLEAN] =
    BEGIN
    fsh: FSHandle = ValidateHandle[sH];
    RETURN[fsh.eofInBuffer AND fsh.index = fsh.dataBytesInBuffer]
    END;

  GetIndex: PUBLIC PROCEDURE [sH: Stream.Handle] RETURNS [LONG CARDINAL] =
    BEGIN
    fsh: FSHandle = ValidateHandle[sH];
    RETURN[Inline.LongMult[fsh.firstFilePageInBuffer-fsh.dataOffset, bytesPerPage] + fsh.index]
    END;

  IndexOutOfRange: PUBLIC SIGNAL [errorStream: Stream.Handle] = CODE;

  SetIndex: PUBLIC PROCEDURE [sH: Stream.Handle, nextByte: LONG CARDINAL] =
    BEGIN
    fsh: FSHandle = ValidateHandle[sH];
    filePage, byte: CARDINAL;
    bufferPage: CARDINAL = fsh.firstFilePageInBuffer;
    bufferPages: CARDINAL = fsh.bufferBytes/bytesPerPage;
    IF (Inline.LongMult[bufferPage-fsh.dataOffset, bytesPerPage] + fsh.index) = nextByte THEN RETURN;
    IF Inline.HighHalf[nextByte] >= bytesPerPage THEN GOTO OutOfRange;
    [filePage, byte] ←
      Inline.LongDivMod[nextByte,bytesPerPage];
    filePage ← filePage + fsh.dataOffset;
    IF filePage NOT IN[bufferPage..bufferPage+bufferPages) THEN
      BEGIN
      growFile: BOOLEAN ← FALSE;
      IF fsh.lengthChanged THEN Cleanup[fsh];
      IF nextByte > fsh.leader.length THEN
        IF ~fsh.grow THEN GOTO OutOfRange
        ELSE
          {fsh.filePages ← filePage + growIncrement; growFile ← TRUE};
      SetupBuffer[fsh,
        MAX[filePage,fsh.dataOffset+swapUnitSize]-swapUnitSize, growFile];
      END;
    fsh.index ←
      (filePage-fsh.firstFilePageInBuffer) * bytesPerPage + byte;
    IF fsh.index > fsh.dataBytesInBuffer THEN
      {fsh.dataBytesInBuffer ← fsh.index; fsh.lengthChanged ← TRUE};
    RETURN;
    EXITS OutOfRange => SIGNAL IndexOutOfRange[sH];
    END;

  GetLength: PUBLIC PROCEDURE [sH: Stream.Handle] RETURNS [LONG CARDINAL] =
    BEGIN
    fsh: FSHandle = ValidateHandle[sH];
    Cleanup[fsh];
    RETURN[fsh.leader.length]
    END;

  SetLength: PUBLIC PROCEDURE [sH: Stream.Handle, fileLength: LONG CARDINAL] =
    BEGIN
    fsh: FSHandle = ValidateHandle[sH];
    newFilePages: CARDINAL;
    growFile: BOOLEAN ← FALSE;
    bufferPage: CARDINAL = fsh.firstFilePageInBuffer;
    bufferPages: CARDINAL = fsh.bufferBytes/bytesPerPage;
    Cleanup[fsh];
    IF Inline.HighHalf[fileLength] >= bytesPerPage THEN GOTO OutOfRange;
    newFilePages ←
      Inline.LongDiv[fileLength+bytesPerPage-1,bytesPerPage]+fsh.dataOffset;
    IF newFilePages > fsh.filePages THEN
      IF fsh.grow THEN {fsh.filePages ← newFilePages; growFile ← TRUE}
      ELSE GOTO OutOfRange;
    fsh.leader.length ← fileLength;
    WriteLeader[fsh];
    SetupBuffer[fsh,
      IF newFilePages IN [bufferPage..bufferPage+bufferPages) THEN fsh.firstFilePageInBuffer
      ELSE MAX[newFilePages, fsh.dataOffset+swapUnitSize] - swapUnitSize, growFile];
    fsh.index ← fsh.dataBytesInBuffer;
    RETURN;
    EXITS OutOfRange => SIGNAL IndexOutOfRange[sH];
    END;

  GetCapability: PUBLIC PROCEDURE [sH: Stream.Handle] RETURNS [File.Capability] =
    {fsh: FSHandle = ValidateHandle[sH]; RETURN[fsh.file]};

  -- LeaderPage

  leaderVersionID: CARDINAL = 01240;

  maxLeaderNameCharacters: CARDINAL = 40;

  LeaderPage: TYPE = MACHINE DEPENDENT RECORD [
    versionID: CARDINAL,
    dataType: FileStream.Subtype,
    create, write, read: System.GreenwichMeanTime,
    length: LONG CARDINAL,
    nameLength: CARDINAL,
    name: PACKED ARRAY [0..maxLeaderNameCharacters) OF CHARACTER];

  InitLeader: PROCEDURE [fsh: FSHandle] =
    BEGIN
    leaderSpace: Space.Handle = Space.Create[1, Space.virtualMemory];
    leader: LONG POINTER TO LeaderPage = Space.LongPointer[leaderSpace];
    hasLeader: BOOLEAN ←
      File.GetAttributes[fsh.file].type = DCSFileTypes.tLeaderPage;
    maxLength: LONG CARDINAL ← Inline.LongMult[fsh.filePages, bytesPerPage];
    IF fsh.filePages = 0 THEN
      IF fsh.grow THEN File.SetSize[fsh.file, fsh.filePages ← growIncrement + 1]
      ELSE hasLeader ← FALSE;
    IF hasLeader THEN
      BEGIN
      leaderCap: File.Capability ← fsh.file;
      now: System.GreenwichMeanTime = System.GetGreenwichMeanTime[];
      leaderCap.permissions ← File.read+File.write;
      IF maxLength # 0 THEN maxLength ← maxLength - bytesPerPage;
      Space.Map[leaderSpace, [leaderCap, 0]];
      fsh.dataOffset ← 1;
      IF leader.versionID # leaderVersionID THEN
        BEGIN
        Zero[leader, Environment.wordsPerPage];
        leader.versionID ← leaderVersionID;
        leader.length ← maxLength;
        fsh.lengthChanged ← TRUE;
        END
      ELSE IF leader.length > maxLength THEN
        {leader.length ← maxLength; fsh.lengthChanged ← TRUE};
      IF fsh.write THEN leader.create ← leader.write ← now;
      IF fsh.read THEN leader.read ← now ELSE leader.read ← [0];
      END
    ELSE
      BEGIN
      Space.Map[leaderSpace];
      Zero[leader, Environment.wordsPerPage];
      leader.length ← maxLength;
      END;
    Space.ForceOut[leaderSpace];
    fsh.leader ← leader;
    RETURN
    END;

  CloseLeader: PROCEDURE [fsh: FSHandle] =
    {Space.Delete[GetMappedSpace[fsh.leader]]; fsh.leader ← NIL};

  SetLeaderProperties: PUBLIC PROCEDURE [
    sH: Stream.Handle,
    create, write, read: System.GreenwichMeanTime ← [0],
    name: STRING ← NIL,
    type: FileStream.Subtype ← null] =
    BEGIN
    fsh: FSHandle = ValidateHandle[sH];
    leader: LONG POINTER TO LeaderPage = fsh.leader;
    ReadLeader[fsh];
    IF create # 0 THEN leader.create ← create;
    IF write # 0 THEN leader.write ← write;
    IF read # 0 THEN leader.read ← read;
    IF name # NIL THEN
      BEGIN
      leader.nameLength ← MIN[LENGTH[leader.name],name.length];
      FOR i: CARDINAL IN [0..leader.nameLength) DO
        leader.name[i] ← name[i];
        ENDLOOP;
      END;
    IF type # null THEN leader.dataType ← type;
    WriteLeader[fsh];
    RETURN
    END;

  GetLeaderProperties: PUBLIC PROCEDURE [sH: Stream.Handle, name: STRING ← NIL]
    RETURNS [type: FileStream.Subtype, create, write, read: System.GreenwichMeanTime] =
    BEGIN
    fsh: FSHandle = ValidateHandle[sH];
    leader: LONG POINTER TO LeaderPage = fsh.leader;
    ReadLeader[fsh];
    IF name # NIL THEN
      BEGIN
      name.length ← MIN[leader.nameLength,name.maxlength];
      FOR i: CARDINAL IN [0..name.length) DO
        name[i] ← leader.name[i];
        ENDLOOP;
      END;
    RETURN[leader.dataType, leader.create, leader.write, leader.read]
    END;

  -- Leader page operations without a stream

  NoLeaderPage: PUBLIC ERROR [errorCap: POINTER TO File.Capability] = CODE;

  GetLeader: PROCEDURE [cap: POINTER TO File.Capability]
    RETURNS [leader: LONG POINTER TO LeaderPage] =
    BEGIN
    leaderSpace: Space.Handle;
    pages: File.PageCount;
    IF File.GetAttributes[cap↑].type # DCSFileTypes.tLeaderPage OR
      (pages←File.GetSize[cap↑]) = 0 THEN ERROR NoLeaderPage[cap];
    leaderSpace ← Space.Create[1, Space.virtualMemory];
    leader ← Space.LongPointer[leaderSpace];
    cap.permissions ← File.read + File.write;
    Space.Map[leaderSpace, [cap↑, 0]];
    IF leader.versionID # leaderVersionID THEN {
      Zero[leader,Environment.wordsPerPage];
      leader.versionID ← leaderVersionID;
      leader.length ← (pages-1)*Environment.bytesPerPage};
    RETURN
    END;

  GetLeaderPropertiesForCapability: PUBLIC PROCEDURE [cap: File.Capability, name: STRING ← NIL]
    RETURNS [type: FileStream.Subtype, create, write, read: System.GreenwichMeanTime, length: LONG CARDINAL] =
    BEGIN
    leader: LONG POINTER TO LeaderPage = GetLeader[@cap];
    IF name # NIL THEN
      BEGIN
      name.length ← MIN[leader.nameLength,name.maxlength];
      FOR i: CARDINAL IN [0..name.length) DO
        name[i] ← leader.name[i];
        ENDLOOP;
      END;
    type ← leader.dataType;
    create ← leader.create;
    write ← leader.write;
    read ← leader.read;
    length ← leader.length;
    Space.Delete[GetMappedSpace[leader]];
    RETURN
    END;

  SetLeaderPropertiesForCapability: PUBLIC PROCEDURE [
    cap: File.Capability,
    create, write, read: System.GreenwichMeanTime ← [0],
    name: STRING ← NIL,
    type: FileStream.Subtype ← null] =
    BEGIN
    leader: LONG POINTER TO LeaderPage = GetLeader[@cap];
    IF create # 0 THEN leader.create ← create;
    IF write # 0 THEN leader.write ← write;
    IF read # 0 THEN leader.read ← read;
    IF name # NIL THEN
      BEGIN
      leader.nameLength ← MIN[LENGTH[leader.name],name.length];
      FOR i: CARDINAL IN [0..leader.nameLength) DO
        leader.name[i] ← name[i];
        ENDLOOP;
      END;
    IF type # null THEN leader.dataType ← type;
    Space.Delete[GetMappedSpace[leader]];
    RETURN
    END;

  -- Utilities

  maxBufferPages: CARDINAL = 70;
  maxBufferBytes: CARDINAL = maxBufferPages * Environment.bytesPerPage;
  swapUnitSize: CARDINAL = 5;
  growIncrement: CARDINAL = 10;

  Cleanup: PROCEDURE [fsh: FSHandle] =
    BEGIN
    Space.ForceOut[fsh.bufferSpace];
    ReadLeader[fsh];
    IF fsh.lengthChanged THEN
      BEGIN
      fsh.leader.length ← Inline.LongMult[fsh.firstFilePageInBuffer-fsh.dataOffset, bytesPerPage] + fsh.dataBytesInBuffer;
      WriteLeader[fsh];
      fsh.lengthChanged ← FALSE;
      END;
    RETURN
    END;
  
  ReadLeader: PROCEDURE [fsh: FSHandle] =
    BEGIN
    leaderSpace: Space.Handle = GetMappedSpace[fsh.leader];
    Space.CopyIn[leaderSpace, Space.GetWindow[leaderSpace] ! Space.Error => CONTINUE];
    END;
  
  WriteLeader: PROCEDURE [fsh: FSHandle] =
    BEGIN
    Space.ForceOut[GetMappedSpace[fsh.leader]];
    END;

  SetupBuffer: PROCEDURE [
    fsh: FSHandle, filePage: CARDINAL, setLength: BOOLEAN ← FALSE] =
    BEGIN
    filePagesFollowing: CARDINAL;
    indexOfStartOfBuffer: LONG CARDINAL;
    IF fsh.buffer = NIL THEN
      BEGIN
      fsh.bufferSpace ← Space.Create[maxBufferPages, Space.virtualMemory];
      Space.CreateUniformSwapUnits[swapUnitSize, fsh.bufferSpace];
      fsh.buffer ← Space.LongPointer[fsh.bufferSpace];
      END
    ELSE EmptyBuffer[fsh, FALSE];
    IF setLength THEN File.SetSize[fsh.file, fsh.filePages];
    Space.Map[fsh.bufferSpace, [fsh.file, filePage]];
    filePagesFollowing ← fsh.filePages - filePage;
    indexOfStartOfBuffer ← LONG[filePage-fsh.dataOffset]*bytesPerPage;
    IF fsh.eofInBuffer ←
      (fsh.leader.length <= indexOfStartOfBuffer + maxBufferBytes) THEN
      BEGIN
      fsh.dataBytesInBuffer ← Inline.LowHalf[
        fsh.leader.length - indexOfStartOfBuffer]
      END
    ELSE fsh.dataBytesInBuffer ← maxBufferBytes;
    fsh.bufferBytes ←
      MIN[filePagesFollowing, maxBufferPages] * bytesPerPage;
    fsh.firstFilePageInBuffer ← filePage;
    RETURN
    END;

  EmptyBuffer: PROCEDURE [fsh: FSHandle, delete: BOOLEAN] =
    BEGIN
    IF fsh.buffer = NIL THEN RETURN;
    Cleanup[fsh];
    IF delete THEN {Space.Delete[fsh.bufferSpace]; fsh.buffer ← NIL}
    ELSE Space.Unmap[fsh.bufferSpace];
    fsh.index ← fsh.bufferBytes ← fsh.dataBytesInBuffer ← 0;
    RETURN
    END;

  AdvanceBuffer: PROCEDURE [fsh: FSHandle] =
    BEGIN
    filePagesInBuffer: CARDINAL = fsh.bufferBytes/bytesPerPage;
    nextPage: CARDINAL = fsh.firstFilePageInBuffer + filePagesInBuffer;
    changeSize: BOOLEAN ← FALSE;
    IF nextPage >= fsh.filePages THEN
      {fsh.filePages ← fsh.filePages + growIncrement; changeSize ← TRUE};
    SetupBuffer[fsh,
      MAX[nextPage,fsh.dataOffset+swapUnitSize]-swapUnitSize, changeSize];
    fsh.index ←
      MIN[(nextPage-fsh.firstFilePageInBuffer) * bytesPerPage, fsh.dataBytesInBuffer];
    RETURN
    END;

  GetMappedSpace: PROCEDURE [p: LONG POINTER] RETURNS [h: Space.Handle] =
    BEGIN
    mapped: BOOLEAN;
    parent: Space.Handle;
    h ← Space.GetHandle[Space.PageFromLongPointer[p]];
    DO
      [parent: parent, mapped: mapped] ← Space.GetAttributes[h];
      IF mapped THEN EXIT;
      h ← parent;
      ENDLOOP;
    RETURN
    END;

  PagesForBytes: PROCEDURE [w: LONG CARDINAL] RETURNS [p: LONG CARDINAL] =
    INLINE {RETURN[(w+bytesPerPage-1)/bytesPerPage]};

  Zero: PROCEDURE [p: LONG POINTER, n: CARDINAL] =
    BEGIN
    IF n = 0 THEN RETURN;
    p↑ ← 0;
    Inline.LongCOPY[from: p, to: p+1, nwords: n-1];
    RETURN
    END;

  END...