-- PilotSSImpl.mesa Edited by: Johnsson on 8-Jan-81 18:12:59 -- Edited by: Russ Atkinson on 20-Mar-81 18:07:23 -- Edited by: Paul Rovner on 30-Jul-81 8:51:01 (seg.spare1 on => file is defaultwindow) -- Edited by: Levin on 8-Dec-81 9:45:40 (bug fixes; monitorize; merge in DummyFileCache) DIRECTORY DCSFileTypes USING [tLeaderPage], Directory USING [ CreateFile, DeleteFile, Error, GetNext, GetProps, ignore, Lookup], Environment USING [PageCount, PageNumber, bytesPerPage, wordsPerPage], File USING [ Capability, GetAttributes, GetSize, grow, Permissions, SetSize, ShowCapability, Type], FileStream USING [ Create, GetCapability, GetLeaderProperties, GetLeaderPropertiesForCapability, InvalidOperation, NoLeaderPage, SetLeaderProperties, SetLeaderPropertiesForCapability], Inline USING [BITAND, BITOR, DIVMOD], Segments, SegmentsExtra USING[], -- exports: NewSegmentForSpace Space USING [ Activate, Create, CreateUniformSwapUnits, Deactivate, Delete, ForceOut, GetAttributes, GetHandle, Handle, Kill, LongPointer, MakeReadOnly, Map, mds, nullHandle, PageFromLongPointer, Unmap, virtualMemory], Storage USING [Node, Pages], Stream USING [InputOptions, SendNow], Streams USING [Address, Handle], String USING [AppendString, AppendSubString, SubStringDescriptor], Time USING [Current, Packed]; PilotSSImpl: MONITOR IMPORTS Directory, File, FileStream, Inline, Segments, Space, Storage, Stream, String, Time EXPORTS SegmentsExtra, Segments, Streams SHARES File, Segments = BEGIN OPEN Segments; -- This module has been more-or-less monitorized, but since the interface isn't well-suited -- to monitors (e.g., consider the enumeration procedures), I've done it in a rather simple -- way. Simply stated, the monitor is intended to protect separate, independent clients of -- this package from screwing each other. However, two clients who share a segment object -- must cooperate externally; this monitor won't protect them. Only the procedures which -- actually manipulate the segTables and fileTables are actually monitored, including the -- enumeration guys. See comments in the enumeration procedures for appropriate caveats. -- I claim that the existing functionality has in no way been reduced, only the potential -- for evil. (RL) SwapError: PUBLIC SIGNAL [s: SHandle] = CODE; InvalidSegment: PUBLIC SIGNAL [s: UNSPECIFIED] = CODE; DeleteSegment: PUBLIC PROCEDURE [seg: SHandle] = BEGIN file: FHandle ← seg.file; SwapOut[seg]; IF seg.space # Space.nullHandle THEN Space.Delete[seg.space]; ReturnSeg[seg]; IF file # NIL THEN IF (file.lock ← file.lock - 1) = 0 THEN ReleaseFile[file]; RETURN END; EnumerateSegments: PUBLIC PROCEDURE [ proc: PROCEDURE [SHandle] RETURNS [BOOLEAN]] RETURNS [f: SHandle] = BEGIN -- Note: once we have a pointer to a page of segment objects, the tail of -- the chain is guaranteed to be good. This is because no pages of the -- objects are ever freed, and new pages are only added at the front. So, -- we needn't stand on our heads to unlock the monitor around the call to -- 'proc'; we just unlock it as soon as we have stashed the head. Of course, -- we can't guarantee that the caller of the enumerator will see a consistent -- snapshot of the segment table, but that guarantee was never present in the -- interface anyway, so no functionality has been lost. Init: ENTRY PROCEDURE RETURNS [STableHandle] = INLINE {RETURN[segTables]}; FOR t: STableHandle ← Init[], t.link UNTIL t = NIL DO FOR i: CARDINAL IN [0..SegsPerTable) DO f ← @t.segs[i]; IF f.inuse AND proc[f] THEN RETURN[f]; ENDLOOP; ENDLOOP; RETURN[NIL] END; FlushSegmentCache: PUBLIC PROCEDURE = {}; Kill: PUBLIC PROCEDURE [seg: SHandle, base: PageNumber ← DefaultBase] = BEGIN IF seg.mapped THEN Space.Kill[seg.space] ELSE seg.killed ← TRUE; RETURN END; MakeReadOnly: PUBLIC PROCEDURE [seg: SHandle] = BEGIN ValidateSeg[seg]; IF seg.mapped THEN Space.MakeReadOnly[seg.space]; seg.write ← FALSE; RETURN END; MoveSegment: PUBLIC PROCEDURE [ seg: SHandle, base: Environment.PageNumber, pages: Environment.PageCount] = BEGIN IF IsVMSeg[seg] THEN ERROR SwapError[seg]; SwapOut[seg]; IF seg.space # Space.nullHandle THEN { IF seg.pages < pages THEN { Space.Delete[seg.space]; seg.space ← Space.nullHandle; seg.mapped ← FALSE} ELSE IF seg.mapped THEN {Space.Unmap[seg.space]; seg.mapped ← FALSE}}; seg.base ← IF base = DefaultBase THEN 1 ELSE base; seg.pages ← pages; IF seg.base # 1 AND File.GetSize[seg.file.cap] < seg.base + pages THEN SIGNAL SwapError[seg]; RETURN END; NewSegment: PUBLIC PROCEDURE [ file: FHandle, base: Environment.PageNumber, pages: Environment.PageCount, access: Access ← DefaultAccess] RETURNS [s: SHandle] = BEGIN LockFile[file]; s ← NewSegmentCommon[access]; s.file ← file; s.base ← IF base = DefaultBase THEN 1 ELSE base; s.pages ← pages; END; NewSegmentForSpace: PUBLIC PROCEDURE [space: Space.Handle, access: Access ← DefaultAccess] RETURNS [s: SHandle] = BEGIN s ← NewSegmentCommon[access]; s.space ← space; s.base ← 1; s.pages ← Space.GetAttributes[space].size; s.mapped ← s.spare1 ← TRUE; END; SegmentAddress: PUBLIC PROCEDURE [seg: SHandle] RETURNS [Address] = BEGIN ValidateSeg[seg]; IF ~seg.mapped THEN ERROR SwapError[seg]; RETURN[Space.LongPointer[seg.space]]; END; SwapIn: PUBLIC PROCEDURE [ seg: SHandle, base: PageNumber ← DefaultANYBase, info: AllocInfo ← EasyUp] = BEGIN OPEN Space; ValidateSeg[seg]; IF seg.lock = MaxSegLocks THEN ERROR SwapError[seg]; IF ~seg.mapped THEN IF IsVMSeg[seg] THEN ERROR SwapError[seg] ELSE BEGIN cap: File.Capability ← seg.file.cap; type: File.Type ← File.GetAttributes[cap].type; pageOffset: CARDINAL ← 1; IF type = DCSFileTypes.tLeaderPage THEN pageOffset ← 0; IF seg.space = Space.nullHandle THEN { seg.space ← Space.Create[ size: seg.pages, parent: IF base = DefaultMDSBase THEN mds ELSE virtualMemory]; IF seg.pages > 7 THEN Space.CreateUniformSwapUnits[parent: seg.space, size: 4]}; cap.permissions ← IF seg.write THEN Read+Write ELSE Read; Space.Map[space: seg.space, window: [file: cap, base: seg.base-pageOffset]]; IF seg.killed THEN Space.Kill[seg.space]; Space.Activate[seg.space]; seg.mapped ← TRUE; seg.killed ← FALSE; IF seg.write THEN { c: Time.Packed = Time.Current[]; SetFileTimes[file: seg.file, create: c, read: c, write: c]} ELSE SetFileTimes[file: seg.file, read: Time.Current[]]; END; seg.lock ← seg.lock+1; RETURN END; SwapOut: PUBLIC PROCEDURE [seg: SHandle] = BEGIN ValidateSeg[seg]; IF ~seg.mapped THEN RETURN; IF seg.lock > 0 THEN ERROR SwapError[seg]; Space.Deactivate[seg.space]; RETURN END; SwapUp: PUBLIC PROCEDURE [seg: SHandle] = BEGIN ValidateSeg[seg]; IF seg.mapped THEN Space.ForceOut[seg.space]; RETURN END; Unlock: PUBLIC PROCEDURE [seg: SHandle] = BEGIN ValidateSeg[seg]; IF seg.lock=0 THEN ERROR SwapError[seg]; seg.lock ← seg.lock-1; RETURN END; VMtoSegment: PUBLIC PROCEDURE [a: Address] RETURNS [SHandle] = BEGIN space, parent: Space.Handle; mapped: BOOLEAN; FindSeg: PROCEDURE [seg: SHandle] RETURNS [BOOLEAN] = BEGIN RETURN[seg.space = space] END; space ← Space.GetHandle[Space.PageFromLongPointer[a]]; DO IF space = Space.mds OR space = Space.virtualMemory THEN RETURN[NIL]; [parent: parent, mapped: mapped] ← Space.GetAttributes[space]; IF mapped THEN EXIT; space ← parent; ENDLOOP; RETURN[EnumerateSegments[FindSeg]]; END; -- File Routines FileError: PUBLIC SIGNAL [f: FHandle] = CODE; InvalidFile: PUBLIC SIGNAL [f: UNSPECIFIED] = CODE; DestroyFile: PUBLIC PROCEDURE [file: FHandle] = BEGIN name: STRING ← [100]; NameForFile[name, file]; Directory.DeleteFile[name]; END; ModifyFile: PUBLIC PROCEDURE [name: STRING] RETURNS [BOOLEAN] = BEGIN cap: File.Capability; okay: BOOLEAN ← TRUE; -- Note: once we have a pointer to a StatItem, the tail of -- the chain is guaranteed to be good. This is because no StatItems -- are ever freed, and new StatItems are only added at the front. So, -- we needn't stand on our heads to unlock the monitor around the call to -- 'proc'; we just unlock it as soon as we have stashed the head. Of course, -- we can't guarantee that ModifyFile will see a consistent -- snapshot of the StatItem list, but that guarantee was never present in the -- interface anyway, so no functionality has been lost. Init: ENTRY PROCEDURE RETURNS [POINTER TO StatItem] = INLINE {RETURN[statHead]}; CheckFHandle: PROCEDURE [file: FHandle] RETURNS [BOOLEAN] = BEGIN IF file.lock # 0 AND File.ShowCapability[cap].fID = File.ShowCapability[file.cap].fID THEN BEGIN FOR p: POINTER TO StatItem ← Init[], p.link UNTIL p = NIL DO okay ← okay AND p.proc[name, file] ENDLOOP; okay ← okay AND (~file.inuse OR file.lock = 0); RETURN[okay]; END; RETURN[FALSE]; END; FOR p: POINTER TO StatItem ← Init[], p.link UNTIL p = NIL DO IF ~(okay ← okay AND p.proc[name, NIL]) THEN EXIT; ENDLOOP; IF okay THEN { cap ← Directory.Lookup[fileName: name, permissions: Directory.ignore ! Directory.Error => GOTO error]; [] ← EnumerateFiles[CheckFHandle]}; RETURN[okay]; EXITS error => RETURN[TRUE]; END; StatItem: TYPE = RECORD [ link: POINTER TO StatItem, proc: PROC[STRING,FHandle] RETURNS [BOOLEAN]]; statHead: POINTER TO StatItem ← NIL; AddModifyProc: PUBLIC ENTRY PROC [proc: PROC[STRING,FHandle] RETURNS [BOOLEAN]] = { p: POINTER TO StatItem = Storage.Node[SIZE[StatItem]]; p↑ ← [link: statHead, proc: proc]; statHead ← p}; NewFile: PUBLIC PROCEDURE [ name: STRING, access: Access ← DefaultAccess, version: VersionOptions ← DefaultVersion] RETURNS [file: FHandle] = BEGIN type: File.Type ← DCSFileTypes.tLeaderPage; old, create: BOOLEAN; cap: File.Capability; dot: CARDINAL = name.length-1; IF name[dot] = '. THEN name.length ← dot; [access,version] ← ValidateOptions[access,version]; create ← version # OldFileOnly; IF create THEN BEGIN bogus: BOOLEAN ← FALSE; old ← FALSE; cap ← Directory.CreateFile[name,type,0 ! Directory.Error => { IF type = fileAlreadyExists THEN old ← TRUE ELSE bogus ← TRUE; CONTINUE}]; IF bogus OR (old AND version = NewFileOnly) THEN ERROR FileNameError[name]; IF old THEN cap ← Directory.Lookup[ fileName: name, permissions: Directory.ignore ! Directory.Error => ERROR FileNameError[name]]; END ELSE cap ← Directory.Lookup[ fileName: name, permissions: Directory.ignore ! Directory.Error => ERROR FileNameError[name]]; file ← InsertFile[@cap, access]; END; FileNameError: PUBLIC SIGNAL [name: STRING] = CODE; ValidateOptions: PROCEDURE [ access: Access, version: VersionOptions] RETURNS [Access, VersionOptions] = BEGIN OPEN Inline; IF access = DefaultAccess THEN access ← Read; -- IF version = DefaultVersion THEN version ← 0; IF BITAND[version, NewFileOnly+OldFileOnly] = NewFileOnly+OldFileOnly OR (BITAND[version, NewFileOnly]#0 AND BITAND[access, File.grow]=0) THEN ERROR FileError[NIL]; IF BITAND[access,File.grow]=0 THEN version ← BITOR[version,OldFileOnly]; RETURN[access, version] END; NameForFile: PUBLIC PROCEDURE [name: STRING, file: FHandle] = BEGIN name.length ← 0; [] ← Directory.GetProps[file.cap, name ! Directory.Error => { String.AppendString[name, "???"L]; CONTINUE}]; RETURN END; EnumerateDirectory: PUBLIC PROC [ proc: PROC [POINTER TO FP, STRING] RETURNS [BOOLEAN], files: STRING ← NIL, wantWholeName: BOOLEAN ← FALSE] = BEGIN next: STRING ← [100]; name: STRING ← [100]; cap: File.Capability; IF files = NIL THEN files ← "*"L; DO cap ← Directory.GetNext[pathName: files, currentName: next, nextName: next ! Directory.Error => CONTINUE]; IF next.length = 0 THEN EXIT; StripQualification[from: next, to: name, all: wantWholeName]; IF proc[@cap, name] THEN EXIT; ENDLOOP; END; StripQualification: PROC [from, to: STRING, all: BOOLEAN] = BEGIN split: String.SubStringDescriptor ← [base: from, length: , offset: ]; IF from.length = 0 THEN RETURN; IF all THEN {String.AppendString[to, from]; RETURN}; FOR i: CARDINAL DECREASING IN [0.. from.length) DO IF from[i] = '> THEN {split.offset ← i+1; EXIT}; ENDLOOP; split.length ← from.length-split.offset; to.length ← 0; String.AppendSubString[to, @split]; StripDot[to]; END; StripDot: PROCEDURE [s: STRING] = INLINE BEGIN dot: CARDINAL; IF s = NIL OR s.length = 0 THEN RETURN; IF s[dot ← (s.length - 1)] = '. THEN s.length ← dot; END; InvalidateFileCache, FlushFileCache: PUBLIC PROC = {}; EnumerateFiles: PUBLIC --ENTRY-- PROCEDURE [ proc: PROCEDURE [FHandle] RETURNS [BOOLEAN]] RETURNS [f: FHandle] = BEGIN -- Note: once we have a pointer to a page of file objects, the tail of -- the chain is guaranteed to be good. This is because no pages of the -- objects are ever freed, and new pages are only added at the front. So, -- we needn't stand on our heads to unlock the monitor around the call to -- 'proc'; we just unlock it as soon as we have stashed the head. Of course, -- we can't guarantee that the caller of the enumerator will see a consistent -- snapshot of the file table, but that guarantee was never present in the -- interface anyway, so no functionality has been lost. Init: ENTRY PROCEDURE RETURNS [FTableHandle] = INLINE {RETURN[fileTables]}; FOR t: FTableHandle ← fileTables, t.link UNTIL t = NIL DO FOR i: CARDINAL IN [0..FilesPerTable) DO f ← @t.files[i]; IF f.inuse AND proc[f] THEN RETURN[f]; ENDLOOP; ENDLOOP; RETURN[NIL] END; GetFileProperties: PUBLIC PROCEDURE [file: FHandle] RETURNS [create, write, read: Time.Packed, length: LONG CARDINAL] = BEGIN [create: create, write: write, read: read, length: length] ← FileStream.GetLeaderPropertiesForCapability[file.cap !FileStream.NoLeaderPage => { create←write←read←[0]; length ← File.GetSize[file.cap] * 512; CONTINUE}] END; GetFileTimes: PUBLIC PROCEDURE [file: FHandle] RETURNS [create, write, read: Time.Packed] = BEGIN [create: create, write: write, read: read] ← FileStream.GetLeaderPropertiesForCapability[file.cap !FileStream.NoLeaderPage => {create←write←read←[0]; CONTINUE}] END; GetFileLength: PUBLIC PROCEDURE [file: FHandle] RETURNS [length: LONG CARDINAL] = BEGIN length ← FileStream.GetLeaderPropertiesForCapability[file.cap !FileStream.NoLeaderPage => { length ← File.GetSize[file.cap] * 512; CONTINUE}].length END; InsertFile: PUBLIC ENTRY PROCEDURE [ file: FPHandle, access: Access ← DefaultAccess] RETURNS [f: FHandle] = BEGIN -- Things are a bit tricky here, since it really is important that -- only a single FHandle exist per file ID. Thus, the enumeration -- and potential subsequent GetFile must be done with the monitor -- locked. Unfortunately, this means we can't use the standard -- EnumerateFiles procedure, so we roll our own local version. FOR t: FTableHandle ← fileTables, t.link UNTIL t = NIL DO FOR i: CARDINAL IN [0..FilesPerTable) DO f ← @t.files[i]; IF f.inuse AND f.cap.fID = file.fID THEN RETURN; ENDLOOP; REPEAT FINISHED => BEGIN f ← GetFile[]; f↑ ← [cap: file↑, inuse: TRUE, lock: 0, link: NIL, spare1: FALSE, spare2: FALSE, spare3: FALSE]; END; ENDLOOP; END; LockFile: PUBLIC PROCEDURE [file: FHandle] = BEGIN IF file.lock = MaxFileLocks THEN ERROR FileError[file]; file.lock ← file.lock + 1; RETURN END; ReleaseFile: PUBLIC PROCEDURE [file: FHandle] = BEGIN IF file.lock # 0 THEN RETURN; ReturnFile[file]; RETURN END; SetFileLength: PUBLIC PROCEDURE [file: FHandle, length: LONG CARDINAL] = BEGIN cap: File.Capability ← file.cap; cap.permissions ← AllAccess; File.SetSize[ file: cap, size: (length+Environment.bytesPerPage-1)/Environment.bytesPerPage] END; SetFileTimes: PUBLIC PROCEDURE [file: FHandle, create, write, read: Time.Packed ← [0]] = BEGIN FileStream.SetLeaderPropertiesForCapability[ cap: file.cap, create: create, write: write, read: read !FileStream.NoLeaderPage => CONTINUE] END; UnlockFile: PUBLIC PROCEDURE [file: FHandle] = BEGIN IF file.lock = 0 THEN ERROR FileError[file]; file.lock ← file.lock - 1; RETURN END; -- Seg Sub-Routines freeSegs: SHandle ← NIL; segTables: STableHandle ← NIL; SegsPerTable: CARDINAL = (Environment.wordsPerPage-1)/SIZE[SObject]; STable: TYPE = RECORD [ link: STableHandle, segs: ARRAY [0..SegsPerTable) OF SObject]; STableHandle: TYPE = POINTER TO STable; NewSegmentCommon: PROCEDURE [access: Access] RETURNS [s: SHandle] = BEGIN s ← GetSeg[]; s.file ← NIL; s.space ← Space.nullHandle; s.inuse ← TRUE; s.mapped ← s.killed ← s.spare1 ← FALSE; s.write ← IF access = DefaultAccess OR access = Read THEN FALSE ELSE TRUE; s.lock ← 0; END; ValidateSeg: PUBLIC ENTRY PROCEDURE [s: SHandle] = BEGIN OPEN Inline; table: STableHandle ← BITAND[s, 177400B]; t: STableHandle; i,j: CARDINAL; FOR t ← segTables, t.link UNTIL t = NIL DO IF t = table THEN EXIT; REPEAT FINISHED => ERROR InvalidSegment[s]; ENDLOOP; [i, j] ← DIVMOD[s-@table.segs[0],SIZE[SObject]]; IF j#0 OR ~s.inuse THEN ERROR InvalidSegment[s]; RETURN END; GetNewSegTable: INTERNAL PROCEDURE = INLINE BEGIN t: STableHandle ← Storage.Pages[1]; i: CARDINAL; FOR i IN [0..SegsPerTable) DO ReturnSegInternal[@t.segs[i]]; ENDLOOP; t.link ← segTables; segTables ← t; RETURN END; GetSeg: ENTRY PROCEDURE RETURNS [f: SHandle] = INLINE BEGIN IF freeSegs = NIL THEN GetNewSegTable[]; f ← freeSegs; freeSegs ← f.link; RETURN END; ReturnSeg: ENTRY PROCEDURE [f: SHandle] = INLINE {ReturnSegInternal[f]}; ReturnSegInternal: INTERNAL PROCEDURE [f: SHandle] = BEGIN f.inuse ← FALSE; f.link ← freeSegs; freeSegs ← f; RETURN END; IsVMSeg: PROCEDURE [seg: SHandle] RETURNS [BOOLEAN] = INLINE {RETURN[seg.spare1]}; -- File Sub-Routines freeFiles: FHandle ← NIL; fileTables: FTableHandle ← NIL; FilesPerTable: CARDINAL = (Environment.wordsPerPage-1)/SIZE[FObject]; FTable: TYPE = RECORD [ link: FTableHandle, files: ARRAY [0..FilesPerTable) OF FObject]; FTableHandle: TYPE = POINTER TO FTable; ValidateFile: PUBLIC ENTRY PROCEDURE [f: FHandle] = BEGIN OPEN Inline; table: FTableHandle ← BITAND[f, 177400B]; t: FTableHandle; i,j: CARDINAL; FOR t ← fileTables, t.link UNTIL t = NIL DO IF t = table THEN EXIT; REPEAT FINISHED => ERROR InvalidFile[f]; ENDLOOP; [i, j] ← DIVMOD[f-@table.files[0],SIZE[FObject]]; IF j#0 OR ~f.inuse THEN ERROR InvalidFile[f]; RETURN END; GetFile: INTERNAL PROCEDURE RETURNS [f: FHandle] = BEGIN IF freeFiles = NIL THEN GetNewFileTable[]; f ← freeFiles; freeFiles ← f.link; f.inuse ← TRUE; RETURN END; ReturnFile: ENTRY PROCEDURE [f: FHandle] = INLINE {ReturnFileInternal[f]}; GetNewFileTable: INTERNAL PROCEDURE = INLINE BEGIN t: FTableHandle ← Storage.Pages[1]; i: CARDINAL; FOR i IN [0..FilesPerTable) DO ReturnFileInternal[@t.files[i]]; ENDLOOP; t.link ← fileTables; fileTables ← t; RETURN END; ReturnFileInternal: INTERNAL PROCEDURE [f: FHandle] = BEGIN f.inuse ← FALSE; f.link ← freeFiles; freeFiles ← f; RETURN END; -- Streams implementation Cleanup: PUBLIC PROCEDURE [h: Streams.Handle] = BEGIN Stream.SendNow[h ! FileStream.InvalidOperation => CONTINUE]; END; CreateStream: PUBLIC PROCEDURE [file: FHandle, access: Access ← Read] RETURNS [Streams.Handle] = BEGIN op: Stream.InputOptions = [ signalEndOfStream: TRUE, signalShortBlock: FALSE, signalLongBlock: FALSE, signalSSTChange: FALSE, terminateOnEndPhysicalRecord: FALSE]; LockFile[file]; RETURN[FileStream.Create[[file.cap.fID, access], op]] END; Destroy: PUBLIC PROCEDURE [h: Streams.Handle] = { file: FHandle = FileFromStream[h]; UnlockFile[file]; IF ReleasableFile[file] THEN ReleaseFile[file]; h.delete[h]}; FileFromStream: PUBLIC PROCEDURE [ h: Streams.Handle] RETURNS [file: FHandle] = { cap: File.Capability ← FileStream.GetCapability[h]; file ← InsertFile[@cap, AllAccess]; IF ReleasableFile[file] THEN LockFile[file]}; GetTimes: PUBLIC PROCEDURE [h: Streams.Handle] RETURNS [create, write, read: Time.Packed] = BEGIN [create: create, write: write, read: read] ← FileStream.GetLeaderProperties[h] END; NewStream: PUBLIC PROCEDURE [name: STRING, access: Access ← Read] RETURNS [Streams.Handle] = BEGIN RETURN[CreateStream[NewFile[name, access], access]] END; SetTimes: PUBLIC PROCEDURE [ h: Streams.Handle, create, write, read: Time.Packed] = BEGIN FileStream.SetLeaderProperties[ sH: h, create: create, write: write, read: read] END; GetBlock: PUBLIC PROCEDURE [ h: Streams.Handle, a: Streams.Address, words: CARDINAL] RETURNS [CARDINAL] = BEGIN NoOptions: Stream.InputOptions = [FALSE, FALSE, FALSE, FALSE, FALSE]; RETURN[(h.get[h, [blockPointer: a, startIndex: 0, stopIndexPlusOne: words*2], NoOptions].bytesTransferred+1)/2]; END; PutBlock: PUBLIC PROCEDURE [ h: Streams.Handle, a: Streams.Address, words: CARDINAL] RETURNS [CARDINAL] = BEGIN h.put[h, [blockPointer: a, startIndex: 0, stopIndexPlusOne: words*2], FALSE]; RETURN[words] END; END.