<> <> <> <> <> <> <> <> <> <> <> DIRECTORY Arpa USING [Address, MyAddress], Basics16, BasicTime USING [GetClockPulses, GMT, Now, nullGMT, Pack, Period, TimeNotKnown], Convert USING [RopeFromCard, CardFromRope], CStrings USING [CString], EnvironmentVariables USING [Get], HostTime USING [ExtendedGMTFromHostTime], IO, PFS USING [ Error, FileType, GetClientProperty, Mutability, NameProc, nullUniqueID, InfoProc, OpenFileFromStream, RopeFromPath, PathFromRope, PFSNameToUnixName, tDirectory, tUnspecified, UniqueID], PFSBackdoor, PFSNames, PFSClass, PFSPrefixMap USING [Insert], PFSPrivate USING [], RefText, Rope USING [Cat, Concat, Equal, IsEmpty, Length, Match, ROPE], UnixErrno, UnixDirectory, UnixFSPrivate USING [ReportFailure], UnixSysCalls, UnixSysCallExtensions USING [SetGetBlocking], UnixTypes, UXStrings ; PFSUXImpl: CEDAR MONITOR LOCKS lock.lock USING lock: REF UXData IMPORTS Arpa, Basics16, BasicTime, Convert, EnvironmentVariables, HostTime, IO, PFS, PFSBackdoor, PFSClass, PFSNames, PFSPrefixMap, RefText, Rope, UnixDirectory, UnixErrno, UnixFSPrivate, UnixSysCalls, UnixSysCallExtensions, UXStrings EXPORTS PFS, PFSPrivate ~ { <> ROPE: TYPE ~ Rope.ROPE; PATH: TYPE ~ PFSNames.PATH; RES: TYPE ~ UnixTypes.RES; CString: TYPE ~ CStrings.CString; OpenFileObject: PUBLIC TYPE = PFSClass.OpenFileObject; -- necessary to establish a type identity for the compiler <> myFlavor: ATOM ¬ $UX; myReadOnlyFlavor: ATOM ¬ $UXRO; versionOfAllFiles: PFSNames.Version ¬ [versionKind: none]; defaultCreateMode: CARD ¬ 0644B; <> uxRoot: PATH ~ PFS.PathFromRope["-ux:/"]; ux0Component: PFSNames.Component ~ PFSNames.Fetch[uxRoot, 0]; <> GetDefaultWDirRope: PUBLIC PROC RETURNS [ wDirRope: ROPE ] ~ { value: ROPE ~ EnvironmentVariables.Get["PWD"]; wDirRope ¬ value.Concat["/"]; }; <> FSData: TYPE ~ REF FSDataObject; FSDataObject: TYPE ~ RECORD [ fs: ROPE, readOnly: BOOL ]; myMaintenanceProcs: PFSClass.MaintenanceProcs ¬ NEW[PFSClass.MaintenanceProcsObject ¬ [ sweep~Sweep, validate~Validate]]; Sweep: PFSClass.SweepProc ~ { NULL }; GetHandle: PFSClass.GetHandleProc -- [fs: ROPE, flavorSpecified: BOOL] RETURNS [h: FSHandle, downMsg: ROPE] -- ~ { IF flavorSpecified THEN [h, downMsg] ¬ GetHandleInner[fs, FALSE] ELSE { h ¬ NIL; downMsg ¬ NIL }; }; GetROHandle: PFSClass.GetHandleProc -- [fs: FS, flavorSpecified: BOOL] RETURNS [h: FSHandle, downMsg: ROPE ¬ NIL] -- ~ { IF flavorSpecified THEN [h, downMsg] ¬ GetHandleInner[fs, TRUE] ELSE { h ¬ NIL; downMsg ¬ NIL }; }; GetHandleInner: PROC [fs: ROPE, readOnly: BOOL] RETURNS [h: PFSClass.FSHandle, downMsg: ROPE ¬ NIL] ~ { <> data: FSData ¬ NEW[FSDataObject ¬ [fs, readOnly]]; IF NOT Rope.IsEmpty[fs] THEN RETURN[NIL, NIL]; <> h ¬ NEW[PFSClass.FSObject ¬ [flavor~myFlavor, name~fs, maintenanceProcs~myMaintenanceProcs, procs~unixProcs, data~data]]; }; Validate: PFSClass.ValidateProc -- [h: FSHandle] RETURNS [obsolete: BOOL, downMsg: ROPE] -- ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; RETURN[ FALSE, NIL ]; -- the local file system is always available }; <> unixProcs: PFSClass.FileManipulationProcs ¬ NEW[PFSClass.FileManipulationProcsObject ¬ [ delete~UXDelete, enumerateForInfo~UXEnumerateForInfo, enumerateForNames~UXEnumerateForNames, lookupName: UXLookupName, copy: UXCopy, setAttributes: UXSetAttributes, setByteCountAndUniqueID: UXSetByteCountAndUniqueID, setClientProperty: UXSetClientProperty, getClientProperty: UXGetClientProperty, enumerateClientProperties: UXEnumerateClientProperties, fileInfo~UXFileInfo, rename~UXRename, read~UXRead, write~UXWrite, open~UXOpen, close~UXClose, store~UXStore, retrieve~UXRetrieve, attach~UXAttach, getInfo~UXGetInfo, pfsNameToUnixName: UXPFSNameToUnixName, caseSensitive: UXCaseSensitive ]]; UXDelete: PFSClass.DeleteProc -- [h, file, wantedUniqueID, proc] -- ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; selfName: ROPE ~ "UXDelete"; FailIfReadOnly[h, file]; file ¬ FixPathForUnix[file]; -- PFS.Error if it's a pattern IF (proc = NIL) OR proc[file, UXGetInfoInner[h, file, wantedUniqueID].uniqueID] THEN { unixFile: ROPE ~ PFS.RopeFromPath[file]; cFile: CString ~ UXO[unixFile]; res: RES ~ UnixSysCalls.Unlink[cFile]; UXR[cFile]; IF res#success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; IF errno = EPERM OR errno = EINVAL THEN PFSBackdoor.ProduceError[accessDenied, MakeMsg[selfName, unixFile, " not deleted: can't delete a directory."]] ELSE { message: ROPE ~ MakeMsg[selfName, "Delete Failed for ", unixFile]; UnixFSPrivate.ReportFailure[errno, message]; }; }; }; }; PFSFileTypeFromUnixType: PROC [ uft: UnixTypes.Fmt ] RETURNS [ pfs: PFS.FileType ] ~ { pfs ¬ SELECT uft FROM dir => PFS.tDirectory, ENDCASE => PFS.tUnspecified; }; UXEnumerateForInfo: PFSClass.EnumerateForInfoProc -- [h: FSHandle, pattern: NAME, proc: InfoProc] -- ~ TRUSTED { data: FSData ¬ NARROW[h.data]; -- sanity check dirPath, unixDirPath: PATH; listOfNames: LIST OF PATH ¬ NIL; selfName: ROPE ~ "UXEnumerateForInfo"; [dirPath, unixDirPath, listOfNames] ¬ EnumerateInner[h, pattern]; FOR each: LIST OF PATH ¬ listOfNames, each.rest WHILE each # NIL DO fileType: PFS.FileType ¬ PFS.tUnspecified; attachedTo: PATH ¬ NIL; uniqueID: PFS.UniqueID; mutability: PFS.Mutability ¬ mutable; unixFile: ROPE ¬ PFS.RopeFromPath[each.first]; cFile: CString ~ UXO[unixFile]; buf: UnixTypes.Stat; bytes: INT; res: RES ~ UnixSysCalls.LStat[cFile, @buf]; IF res # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; SELECT errno FROM EPERM, ENOENT, ENXIO, EACCES, ENODEV, ENOTDIR, EISDIR => NULL; -- ignore things we can't look at ENDCASE => { message: ROPE ~ MakeMsg[selfName, "LStat failed (during enumeration) for ", unixFile]; UnixFSPrivate.ReportFailure[errno, message]; }; }; IF buf.mode.fmt=lnk THEN { res: RES ~ UnixSysCalls.Stat[cFile, @buf]; IF res # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; SELECT errno FROM EPERM, ENOENT, ENXIO, EACCES, ENODEV, ENOTDIR, EISDIR => bytes ¬ -1; -- ignore things we can't look at ENDCASE => { message: ROPE ~ MakeMsg[selfName, "Stat failed (during enumeration) for ", unixFile]; UnixFSPrivate.ReportFailure[errno, message]; }; } ELSE bytes ¬ buf.size; attachedTo ¬ GetAttachment[cFile]; attachedTo ¬ ResolveAttachment[each.first, attachedTo]; } ELSE bytes ¬ buf.size; fileType ¬ PFSFileTypeFromUnixType[buf.mode.fmt]; uniqueID ¬ UIDFromMTime[buf.mtime]; UXR[cFile]; IF NOT proc[each.first, attachedTo, uniqueID, bytes, mutability, fileType] THEN EXIT; ENDLOOP; }; UXEnumerateForNames: PFSClass.EnumerateForNamesProc -- [h: FSHandle, pattern: NAME, proc: NameProc] -- ~ { dirPath: PATH; listOfNames: LIST OF PATH ¬ NIL; [dirPath~dirPath, listOfNames~listOfNames] ¬ EnumerateInner[h, pattern]; FOR each: LIST OF PATH ¬ listOfNames, each.rest WHILE each # NIL DO IF NOT proc[each.first] THEN EXIT; ENDLOOP; }; EnumerateInner: PROC [h: PFSClass.FSHandle, pattern: PATH] RETURNS [dirPath, unixDirPath: PATH ¬ NIL, listOfNames: LIST OF PATH ¬ NIL] ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; selfName: ROPE ~ "EnumerateInner"; patternPart: PATH; [dirPath, patternPart] ¬ ParsePattern[pattern]; unixDirPath ¬ FixPathForUnix[dirPath]; { dirRope: ROPE ~ PFS.RopeFromPath[unixDirPath]; dirString: CString ~ UXO[dirRope]; patternRope: ROPE ~ PFS.RopeFromPath[patternPart]; dirp: UnixDirectory.DirPtr ~ UnixDirectory.OpenDir[dirString]; errno: UnixErrno.Errno; UXR[dirString]; IF dirp=NIL AND (errno _ UnixErrno.GetErrno[]) = $ENOENT THEN RETURN; IF dirp=NIL THEN UnixFSPrivate.ReportFailure[errno, MakeMsg["EnumerateInner", "Directory open failed in EnumerateInner for ", dirRope]]; TRUSTED { entryp: UnixDirectory.DirEntPtr ¬ UnixDirectory.ReadDir[dirp]; WHILE entryp # NIL DO name: ROPE ¬ UnixDirectory.NameFromDirEntP[entryp]; IF NOT Rope.Equal[name, ".", TRUE] AND NOT Rope.Equal[name, "..", TRUE] THEN { IF Rope.Match[patternRope, name, TRUE] THEN { fullName: PATH ~ PFSNames.Cat[dirPath, PFS.PathFromRope[name]]; listOfNames ¬ CONS[fullName, listOfNames]; }; }; entryp ¬ UnixDirectory.ReadDir[dirp]; ENDLOOP; }; [] ¬ UnixDirectory.CloseDir[dirp]; }; listOfNames ¬ SortNameList[listOfNames]; }; UXFileInfo: PFSClass.FileInfoProc -- [h: FSHandle, file: PATH, wantedUniqueID: UniqueID] RETURNS [version: Version, attachedTo: PATH, bytes: INT, uniqueID: UniqueID, mutability: PFS.Mutability, fileType: PFS.FileType] -- ~ { RETURN UXGetInfoInner[h, FixPathForUnix[file], wantedUniqueID]; }; UXGetInfoInner: PFSClass.FileInfoProc -- [h: FSHandle, file: PATH, wantedUniqueID: UniqueID] RETURNS [version: Version, attachedTo: PATH, bytes: INT, uniqueID: UniqueID, mutability: PFS.Mutability, fileType: PFS.FileType] -- ~ TRUSTED { data: FSData ¬ NARROW[h.data]; -- sanity check; unixFile: ROPE ¬ PFS.RopeFromPath[file]; selfName: ROPE ~ "UXGetInfoInner"; buf: UnixTypes.Stat; cFile: CString ~ UXO[unixFile]; res: RES ~ UnixSysCalls.LStat[cFile, @buf]; IF res # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ Rope.Concat["Stat failed (in UXGetInfoInner) for ", unixFile]; UnixFSPrivate.ReportFailure[errno, message]; }; IF buf.mode.fmt=lnk THEN { res: RES ~ UnixSysCalls.Stat[cFile, @buf]; IF res # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; SELECT errno FROM EPERM, ENOENT, ENXIO, EACCES, ENODEV, ENOTDIR, EISDIR => bytes ¬ -1; -- ignore things we can't look at ENDCASE => { message: ROPE ~ MakeMsg[selfName, "Stat failed for ", unixFile]; UnixFSPrivate.ReportFailure[errno, message]; }; } ELSE bytes ¬ buf.size; attachedTo ¬ GetAttachment[cFile]; attachedTo ¬ ResolveAttachment[file, attachedTo]; } ELSE { bytes ¬ buf.size; attachedTo ¬ NIL; }; UXR[cFile]; fileType ¬ PFSFileTypeFromUnixType[buf.mode.fmt]; uniqueID ¬ UIDFromMTime[buf.mtime]; IF ( wantedUniqueID # PFS.nullUniqueID ) AND ( uniqueID # wantedUniqueID ) THEN PFSBackdoor.ProduceError[unknownUniqueID, "UniqueID wrong"]; mutability ¬ mutable; version ¬ versionOfAllFiles; <> }; UXGetInfo: PFSClass.GetInfoProc -- [h: FSHandle, file: OpenFile] RETURNS [fullFName, attachedTo: PATH, uniqueID: UniqueID, bytes: INT, mutability: PFS.Mutability, fileType: PFS.FileType] -- ~ TRUSTED { data: REF UXData ~ NARROW[file.data]; fd: UnixTypes.FD ¬ data.fd; buf: UnixTypes.Stat; res: RES ~ UnixSysCalls.FStat[fd, @buf]; IF res # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg["UXGetInfo", "Stat failed for ", file.fullFName]; UnixFSPrivate.ReportFailure[errno, message]; }; RETURN[file.fullFName, file.attachedTo, file.uniqueID, buf.size, file.mutability, PFSFileTypeFromUnixType[buf.mode.fmt]]; }; UXRename: PFSClass.RenameProc -- [h: FSHandle, fromFile: NAME, wantedUniqueID: UniqueID, toFile: NAME, keep: CARDINAL, proc: PFS.NameConfirmProc] RETURNS [done: BOOL ¬ FALSE] -- ~ { data: FSData ¬ NARROW[h.data]; -- sanity check fullName: PATH; uniqueID: PFS.UniqueID; FailIfReadOnly[h, fromFile]; fromFile ¬ FixPathForUnix[fromFile]; -- PFS.Error if it's a pattern toFile ¬ FixPathForUnix[toFile]; [uniqueID~uniqueID] ¬ UXGetInfoInner[h, fromFile, wantedUniqueID]; -- PFS.Error if wantedUniqueID not available IF (proc = NIL) OR proc[fromFile, uniqueID] THEN { -- fromFile or toFile? unixFromFile: ROPE ¬ PFS.RopeFromPath[fromFile]; unixToFile: ROPE ¬ PFS.RopeFromPath[toFile]; cFromFile: CString ~ UXO[unixFromFile]; cToFile: CString ~ UXO[unixToFile]; res: RES ¬ UnixSysCalls.Rename[cFromFile, cToFile]; UXR[cFromFile]; UXR[cToFile]; RETURN[res=success]; }; }; bufferBytes: CARDINAL ¬ 8192; UXStore: PUBLIC PFSClass.StoreProc -- [h: FSHandle, file: NAME, wantedUniqueID: UniqueID, str: IO.STREAM, proc: PFS.StoreConfirmProc, createOptions: PFS.CreateOptions] -- ~ { selfName: ROPE ~ "UXStore"; DoStore: PROC [fd: UnixTypes.FD] ~ TRUSTED { buffer: REF TEXT; TRUSTED { ENABLE UNWIND => { IF buffer # NIL THEN RefText.ReleaseScratch[buffer]; }; buffer ¬ RefText.ObtainScratch[bufferBytes]; DO bytesWritten: INT; ptr: UnixTypes.CHARPtr ~ LOOPHOLE[LOOPHOLE[buffer, UnixTypes.CHARPtr] +SIZE[TEXT[0]]]; buffer.length ¬ IO.GetBlock[str, buffer, 0, buffer.maxLength]; IF buffer.length = 0 THEN EXIT; bytesWritten ¬ UnixSysCalls.Write[fd, ptr, buffer.length]; IF bytesWritten # buffer.length THEN { IF bytesWritten < 0 THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg[selfName, "Write failed for", file]; UnixFSPrivate.ReportFailure[errno, message]; } ELSE PFSBackdoor.ProduceError[inconsistent, "Invalid assumption about UnixSysCalls.Write in Store"]; }; ENDLOOP; { res: RES ~ UnixSysCalls.FSync[fd]; errno: UnixErrno.Errno; IF res#success AND (errno _ UnixErrno.GetErrno[]) # EBADF THEN UnixFSPrivate.ReportFailure[errno, MakeMsg[selfName, "File system full? FSync failed for", file]]; }; RefText.ReleaseScratch[buffer]; buffer ¬ NIL; }; }; openFile: PFSClass.OpenFile ~ UXOpen[h, file, wantedUniqueID, create, FALSE, PFS.tUnspecified, createOptions]; data: REF UXData ~ NARROW[openFile.data]; fd: UnixTypes.FD ~ data.fd; IF proc # NIL THEN [] ¬ proc[openFile.fullFName]; DoStore[fd]; { <> <> ENABLE PFS.Error => CONTINUE; oldMode: ROPE ¬ PFS.GetClientProperty[PFS.OpenFileFromStream[str], "UnixMode"]; newMode: ROPE ¬ UXGetClientProperty[h, openFile, "UnixMode"]; IF oldMode # NIL AND newMode # NIL THEN { oldModeBits: CARD16 ~ Convert.CardFromRope[oldMode]; newModeBits: CARD16 ~ Convert.CardFromRope[newMode]; newerModeBits: CARD16 ~ Basics16.BITOR[newModeBits, Basics16.BITAND[oldModeBits, 111B]]; newerMode: ROPE ~ Convert.RopeFromCard[newerModeBits, 8]; UXSetClientProperty[h, openFile, "UnixMode", newerMode]; }; }; UXClose[h, openFile, FALSE]; }; UXRetrieve: PFSClass.RetrieveProc ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; PFSBackdoor.ProduceError[notImplemented, "Retrieve not yet implemented for -ux view"]; }; NotYetImpl: ERROR ~ CODE; UXLookupName: PFSClass.LookupNameProc -- [h: FSHandle, file: NAME] RETURNS[NAME] -- ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; success: BOOL ¬ TRUE; [] ¬ UXGetInfoInner[h, FixPathForUnix[file], PFS.nullUniqueID ! PFS.Error => IF error.code = $unknownFile THEN {success ¬ FALSE; CONTINUE} ELSE REJECT ]; IF success THEN RETURN[file] ELSE RETURN[NIL]; }; UXCopy: PFSClass.CopyProc -- [h: FSHandle, fromFile: NAME, wantedUniqueID: UniqueID, toFile: NAME, keep: CARDINAL, proc: PFS.NameConfirmProc] RETURNS [done: BOOL ¬ FALSE] -- ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; done ¬ FALSE; -- let the higher level do the copying }; UXSetAttributes: PFSClass.SetAttributesProc -- [h: FSHandle, file: OpenFile, attributes: PFS.CreateOptions] -- ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; PFSBackdoor.ProduceError[notImplemented, "SetAttributes not implemented in UX view"]; }; UXSetByteCountAndUniqueID: PFSClass.SetByteCountAndUniqueIDProc -- [h: FSHandle, file: OpenFile, bytes: INT, uniqueID: PFS.UniqueID] -- ~ { data: FSData ¬ NARROW[h.data]; -- sanity check; fd: UnixTypes.FD ¬ NARROW[file.data, REF UXData].fd; IF bytes>=0 THEN { res: RES ~ UnixSysCalls.FTruncate[fd, bytes]; IF res#success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg["UXSetByteCountAndUniqueID", "failed to change byteCount of", file]; UnixFSPrivate.ReportFailure[errno, message] }; }; <> <> <> <> <> <<};>> }; UXGetClientProperty: PFSClass.GetClientPropertyProc -- [h: FSHandle, file: OpenFile, propertyName: ROPE] RETURNS [propertyValue: ROPE] -- ~ TRUSTED { data: FSData ¬ NARROW[h.data]; -- sanity check; fd: UnixTypes.FD ¬ NARROW[file.data, REF UXData].fd; buf: UnixTypes.Stat; res: RES ~ UnixSysCalls.FStat[fd, @buf]; IF res#success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg["UXGetClientProperty", "Stat failed for", file]; UnixFSPrivate.ReportFailure[errno, message]; }; SELECT TRUE FROM Rope.Equal[propertyName, "UnixMode"] => { RETURN[Convert.RopeFromCard[LOOPHOLE[buf.mode, CARD16], 8]]; }; Rope.Equal[propertyName, "UnixOwner"] => { RETURN[Convert.RopeFromCard[buf.uid]]; }; Rope.Equal[propertyName, "UnixGroup"] => { RETURN[Convert.RopeFromCard[buf.gid]]; }; ENDCASE => RETURN[NIL]; }; UXSetClientProperty: PUBLIC PFSClass.SetClientPropertyProc -- [h: FSHandle, file: OpenFile, propertyName: ROPE, propertyValue: ROPE] -- = { data: FSData ¬ NARROW[h.data]; -- sanity check; fd: UnixTypes.FD ¬ NARROW[file.data, REF UXData].fd; SELECT TRUE FROM Rope.Equal[propertyName, "UnixMode"] => { cardMode: CARD16 ~ Convert.CardFromRope[propertyValue, 8]; res: RES ~ UnixSysCalls.FChMod[fd, LOOPHOLE[cardMode]]; }; Rope.Equal[propertyName, "UnixOwner"] => { PFSBackdoor.ProduceError[notImplemented, "The UnixOwner cannot be changed in the UX view"]; }; Rope.Equal[propertyName, "UnixGroup"] => { PFSBackdoor.ProduceError[notImplemented, "The UnixGroup cannot be changed in the UX view"]; }; ENDCASE => PFSBackdoor.ProduceError[notImplemented, Rope.Concat[propertyName, " is not an implemented property in the UX view"]]; }; UXEnumerateClientProperties: PFSClass.EnumerateClientPropertiesProc -- [h: FSHandle, file: OpenFile, proc: PFS.PropProc] -- ~ TRUSTED { data: FSData ¬ NARROW[h.data]; -- sanity check; fd: UnixTypes.FD ¬ NARROW[file.data, REF UXData].fd; buf: UnixTypes.Stat; res: RES ~ UnixSysCalls.FStat[fd, @buf]; IF NOT proc["UnixGroup", Convert.RopeFromCard[buf.gid]] THEN RETURN; IF NOT proc["UnixMode", Convert.RopeFromCard[LOOPHOLE[buf.mode, CARD16], 8]] THEN RETURN; [] ¬ proc["UnixOwner", Convert.RopeFromCard[buf.uid]]; }; UXRead: UNSAFE PROC [h: PFSClass.FSHandle, file: PFSClass.OpenFile, filePosition, nBytes: CARD, toPtr: LONG POINTER, toStart: CARD] RETURNS [bytesRead: INT] ~ { fs: FSData ~ NARROW[h.data]; -- sanity check data: REF UXData ~ NARROW[file.data]; fd: UnixTypes.FD ¬ data.fd; failure: ROPE; errno: UnixErrno.Errno; selfName: ROPE ~ "UXRead"; DoRead: ENTRY PROC [lock: REF UXData] RETURNS [failure: ROPE ¬ NIL, errno: UnixErrno.Errno ¬ ok] ~ TRUSTED { ENABLE UNWIND => NULL; fdIndex: PFSBackdoor.FDIndex ~ data.fdIndex; data.fdIndex ¬ PFSBackdoor.unreliableIndex; IF filePosition # fdIndex THEN { res: INT ~ UnixSysCalls.LSeek[fd, filePosition, set]; IF res < 0 THEN { errno ¬ UnixErrno.GetErrno[]; failure ¬ MakeMsg[selfName, "LSeek failed for", file]; RETURN[failure, errno]; }; }; bytesRead ¬ UnixSysCalls.Read[d~fd, buf~LOOPHOLE[toPtr+toStart], nBytes~nBytes]; IF bytesRead < 0 THEN { errno ¬ UnixErrno.GetErrno[]; failure ¬ MakeMsg[selfName, "Read failed for", file]; RETURN[failure, errno]; }; IF fdIndex # PFSBackdoor.unreliableIndex THEN data.fdIndex ¬ filePosition+bytesRead; }; bytesRead ¬ 0; [failure, errno] ¬ DoRead[data]; IF failure # NIL THEN UnixFSPrivate.ReportFailure[errno, failure]; }; UXWrite: PROC [h: PFSClass.FSHandle, file: PFSClass.OpenFile, filePosition, nBytes: CARD, fromPtr: LONG POINTER, fromStart: CARD] RETURNS [bytesWritten: INT] ~ { fs: FSData ¬ NARROW[h.data]; -- sanity check data: REF UXData ~ NARROW[file.data]; fd: UnixTypes.FD ~ data.fd; failure: ROPE; errno: UnixErrno.Errno; selfName: ROPE ~ "UXWrite"; DoWrite: ENTRY PROC [lock: REF UXData] RETURNS [failure: ROPE ¬ NIL, errno: UnixErrno.Errno ¬ ok] ~ { ENABLE UNWIND => NULL; fdIndex: PFSBackdoor.FDIndex ~ data.fdIndex; data.fdIndex ¬ PFSBackdoor.unreliableIndex; IF filePosition # fdIndex THEN { res: INT ~ UnixSysCalls.LSeek[fd, filePosition, set]; IF res < 0 THEN { errno ¬ UnixErrno.GetErrno[]; failure ¬ MakeMsg[selfName, "LSeek failed for", file]; RETURN[failure, errno]; }; }; bytesWritten ¬ UnixSysCalls.Write[d~fd, buf~LOOPHOLE[fromPtr+fromStart], nBytes~nBytes]; IF bytesWritten < 0 THEN { errno ¬ UnixErrno.GetErrno[]; failure ¬ MakeMsg[selfName, "Write failed for", file]; RETURN[failure, errno]; }; IF fdIndex # PFSBackdoor.unreliableIndex THEN data.fdIndex ¬ filePosition+bytesWritten; }; bytesWritten ¬ 0; [failure, errno] ¬ DoWrite[data]; IF failure # NIL THEN UnixFSPrivate.ReportFailure[errno, failure]; }; mutable: UnixTypes.Permission ~ [read: true, write: true, exec: false]; immutable: UnixTypes.Permission ~ [read: true, write: false, exec: false]; mutableMode: UnixTypes.Mode ~ [owner: mutable, group: mutable, others: mutable]; <> immutableMode: UnixTypes.Mode ~ [owner: mutable, group: mutable, others: mutable]; -- because, temporarily, the DF software doesn't make the necessary files mutable, and the compiler can't deal with immutable files <<>> UXData: TYPE ~ PFSBackdoor.UXData; maxAttachmentLength: CARD ~ 1000; GetAttachment: PROC [fHandle: CString] RETURNS [attachedTo: PATH] ~ TRUSTED { buf: CString ~ UXStrings.ObtainScratch[1000]; linkSize: INT ~ UnixSysCalls.ReadLink[fHandle, buf, maxAttachmentLength]; IF linkSize<0 THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg["GetAttachment", "ReadLink failed for", UXStrings.ToRope[fHandle]]; UnixFSPrivate.ReportFailure[errno, message]; }; attachedTo ¬ PFS.PathFromRope[UXStrings.ToRope[buf, linkSize]]; UXR[buf]; }; ResolveAttachment: PROC [attachment: PATH, attachedTo: PATH] RETURNS [PATH] ~ { { absolute: PATH ~ IF PFSNames.IsAbsolute[attachedTo] THEN attachedTo ELSE PFSNames.Cat[PFSNames.Directory[attachment], attachedTo]; RETURN[ PFSNames.ReplaceComponent[absolute, 0, ux0Component] ]; }; }; UXOpen: PFSClass.OpenProc -- PROCEDURE [h: FSHandle, file: NAME, wantedUniqueID: UniqueID, access: PFS.AccessOptions, checkFileType: BOOL, fileType: PFS.FileType, createOptions: PFS.CreateOptions] RETURNS [OpenFile] -- ~ { openFile: PFSClass.OpenFile ¬ NEW[PFSClass.OpenFileObject ¬ [fs~h, uniqueID~PFS.nullUniqueID, access~access, state~open, bytes: 0, mutability: mutable, fileType: PFS.tUnspecified]]; fileName: PATH ¬ FixPathForUnix[file]; unixFileName: ROPE ~ PFS.RopeFromPath[fileName]; fHandle: CString ¬ UXS[unixFileName]; fd: UnixTypes.FD; mode: UnixTypes.Mode; selfName: ROPE ~ "UXOpen"; SELECT access FROM read => TRUSTED { buf: UnixTypes.Stat; fd ¬ UnixSysCalls.Open[path~fHandle, flags~[access~RDONLY], mode~immutableMode ]; IF fd=error THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg["UXOpen", "Open failed for", unixFileName]; UnixFSPrivate.ReportFailure[errno, message]; }; openFile.data ¬ NEW[UXData ¬ [fd~fd, fdIndex~0]]; }; write, append => TRUSTED { buf: UnixTypes.Stat; res: RES ¬ UnixSysCalls.LStat[fHandle, @buf]; IF res=success THEN { IF buf.mode.fmt=lnk THEN { PFSBackdoor.ProduceError[accessDenied, MakeMsg[selfName, "Can't open an attachment for writing or appending", unixFileName]]; }; }; fd ¬ UnixSysCalls.Open[path~fHandle, flags~[excl~false, trunc~false, creat~true, access~RDWR], mode~IF createOptions.mutability=mutable THEN mutableMode ELSE immutableMode ]; IF fd=error THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg[selfName, "Open failed for", unixFileName]; UnixFSPrivate.ReportFailure[errno, message]; }; IF access=write THEN openFile.data ¬ NEW[UXData ¬ [fd~fd, fdIndex~0]] ELSE openFile.data ¬ NEW[UXData ¬ [fd]]; }; create => TRUSTED { shortName: ROPE ~ PFSNames.ComponentRope[PFSNames.ShortName[fileName]]; backupLastComp: PFSNames.Component ~ [name: [base: shortName.Concat["~"], start: 0, len: shortName.Length[]+1]]; backupName: PATH ~ PFSNames.ReplaceShortName[fileName, backupLastComp]; backupUnixName: ROPE ~ PFS.RopeFromPath[backupName]; tempLastCompName: ROPE ~ CreateTempName[shortName]; tempLastComp: PFSNames.Component ~ [name: [base: tempLastCompName, start: 0, len: tempLastCompName.Length[]]]; tempName: PATH ~ PFSNames.ReplaceShortName[fileName, tempLastComp]; tempUnixName: ROPE ~ PFS.RopeFromPath[tempName]; tempFHandle: CString ~ UXS[tempUnixName]; cBackupName: CString ~ UXO[backupUnixName]; buf: UnixTypes.Stat; res: RES ¬ UnixSysCalls.LStat[fHandle, @buf]; IF res#success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; IF errno # ENOENT THEN { message: ROPE ~ MakeMsg[selfName, "Stat failed for", unixFileName]; UnixFSPrivate.ReportFailure[errno, message]; }; }; IF buf.mode.fmt=dir THEN { message: ROPE ~ Rope.Cat["Creating a file with the same name (", unixFileName, " ) as an existing directory is not allowd."]; PFSBackdoor.ProduceError[accessDenied, message]; }; BEGIN ok: BOOL ¬ FALSE; fd: UnixTypes.FD ¬ UnixSysCalls.Open[path~fHandle, flags~[access~RDONLY], mode~immutableMode ]; IF fd#error THEN { res ¬ UnixSysCalls.FStat[fd, @buf]; IF res#success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg[selfName, "FStat failed for", unixFileName]; UnixFSPrivate.ReportFailure[errno, message]; }; res ¬ UnixSysCalls.Close[fd]; ok ¬ TRUE; }; IF ok AND (createOptions.setMutability=inherit) THEN mode ¬ buf.mode ELSE { SELECT TRUE FROM createOptions.mutability=mutable => mode ¬ mutableMode; ENDCASE => mode ¬ immutableMode; IF ok THEN { -- fold in execute bits from buf.mode mode.owner.exec ¬ buf.mode.owner.exec; mode.group.exec ¬ buf.mode.group.exec; mode.others.exec ¬ buf.mode.others.exec; }; }; END; IF createOptions.keep # 1 THEN { res ¬ UnixSysCalls.Rename[fHandle, cBackupName]; UXR[cBackupName]; IF res#success THEN { err: UnixErrno.Errno ~ UnixErrno.GetErrno[]; IF err # ENOENT THEN { message: ROPE ~ MakeMsg[selfName, Rope.Cat["Rename failed for ", unixFileName, " to"], backupUnixName]; UnixFSPrivate.ReportFailure[err, message]; }; }; } ELSE { UXR[cBackupName]; res ¬ UnixSysCalls.Unlink[fHandle]; IF res#success THEN { err: UnixErrno.Errno ~ UnixErrno.GetErrno[]; IF err # ENOENT THEN { message: ROPE ~ MakeMsg[selfName, "Unlink failed for", unixFileName]; UnixFSPrivate.ReportFailure[err, message]; }; }; }; fd ¬ UnixSysCalls.Open[path~tempFHandle, flags~[excl~true, creat~true, access~WRONLY], mode~mode ]; IF fd=error THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg[selfName, "Open failed for", tempUnixName]; UnixFSPrivate.ReportFailure[errno, message]; }; openFile.data ¬ NEW[UXData ¬ [fd, tempFHandle, wantedUniqueID, fHandle, 0]]; fHandle ¬ tempFHandle; }; ENDCASE => ERROR; TRUSTED { buf: UnixTypes.Stat; res: RES ~ UnixSysCalls.Stat[fHandle, @buf]; IF res # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg[selfName, "Stat failed for", unixFileName]; UnixFSPrivate.ReportFailure[errno, message]; }; openFile.bytes ¬ buf.size; IF openFile.uniqueID = PFS.nullUniqueID THEN openFile.uniqueID ¬ UIDFromMTime[buf.mtime]; }; { res: RES ~ UnixSysCallExtensions.SetGetBlocking[fd, allData]; IF res # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg[selfName, "SetGetBlocking failed for", unixFileName]; UnixFSPrivate.ReportFailure[errno, message]; }; }; openFile.fullFName ¬ fileName; RETURN [openFile]; }; UXAttach: PFSClass.AttachProc -- PROCEDURE [h: FSHandle, file: PATH, to: PATH, keep: CARDINAL, wantedUniqueID: UniqueID, remoteCheck: BOOL ¬ TRUE] RETURNS [toFName: PATH] -- ~ TRUSTED { selfName: ROPE ~ "UXAttach"; fileName: PATH ¬ FixPathForUnix[file]; unixFileName: ROPE ~ PFS.RopeFromPath[fileName]; fHandle: CString ¬ UXS[unixFileName]; shortName: ROPE ~ PFSNames.ComponentRope[PFSNames.ShortName[fileName]]; backupLastComp: PFSNames.Component ~ [name: [base: shortName.Concat["~"], start: 0, len: shortName.Length[]+1]]; backupName: PATH ~ PFSNames.ReplaceShortName[fileName, backupLastComp]; backupUnixName: ROPE ~ PFS.RopeFromPath[backupName]; cBackupName: CString ~ UXO[backupUnixName]; buf: UnixTypes.Stat; res: RES ¬ UnixSysCalls.LStat[fHandle, @buf]; IF res#success THEN { err: UnixErrno.Errno ~ UnixErrno.GetErrno[]; IF err # ENOENT THEN { message: ROPE ~ MakeMsg[selfName, "Stat failed for", unixFileName]; UnixFSPrivate.ReportFailure[err, message]; }; }; IF buf.mode.fmt=dir THEN { message: ROPE ~ Rope.Cat["Creating a file with the same name (", unixFileName, " ) as an existing directory is not allowed."]; PFSBackdoor.ProduceError[accessDenied, message]; }; res ¬ UnixSysCalls.Rename[fHandle, cBackupName]; UXR[cBackupName]; IF res#success THEN { err: UnixErrno.Errno ~ UnixErrno.GetErrno[]; IF err # ENOENT THEN { message: ROPE ~ MakeMsg[selfName, Rope.Cat["Rename failed for ", unixFileName, " to"], backupUnixName]; UnixFSPrivate.ReportFailure[err, message]; }; }; { toFileName: ROPE ~ PFS.PFSNameToUnixName[to]; toFHandle: CString ~ UXO[toFileName]; IF toFileName=NIL THEN PFSBackdoor.ProduceError[unknownFile, Rope.Cat["Couldn't deduce a unix name for ", PFS.RopeFromPath[to], " in UXAttach."]]; res ¬ UnixSysCalls.SymLink[name1~toFHandle, name2~fHandle]; IF res#success THEN { err: UnixErrno.Errno ~ UnixErrno.GetErrno[]; message: ROPE ~ MakeMsg[selfName, Rope.Cat["SymLink failed from ", unixFileName, " to"], toFileName]; UnixFSPrivate.ReportFailure[err, message]; }; <> UXR[toFHandle]; }; RETURN[file]; }; UXS: PROC [rope: ROPE] RETURNS [CString] ~ INLINE { RETURN [UXStrings.Create[rope]]; }; UXO: PROC[r: REF] RETURNS [CString] ~ INLINE{ RETURN[UXStrings.CreateScratch[r]]; }; <<>> UXR: PROC[s: CString] ~ INLINE{ UXStrings.ReleaseScratch[s]; }; <<>> UXClose: PFSClass.CloseProc -- PROCEDURE [h: FSHandle, file: OpenFile, abort: BOOL] -- ~ { fsData: FSData ¬ NARROW[h.data]; -- sanity check data: REF UXData ¬ NARROW[file.data]; fd: UnixTypes.FD ¬ data.fd; res: RES; failure: ROPE; errno: UnixErrno.Errno; selfName: ROPE ~ "UXClose"; DoClose: ENTRY PROC [lock: REF UXData] RETURNS [failure: ROPE ¬ NIL, errno: UnixErrno.Errno ¬ ok] ~ { ENABLE UNWIND => NULL; data.fdIndex ¬ PFSBackdoor.unreliableIndex; IF data.setUniqueIDAtClose#NIL THEN FixUID[data.setUniqueIDAtClose, IF data.wantedUniqueID = PFS.nullUniqueID THEN file.uniqueID ELSE data.wantedUniqueID]; <> IF file.access#read THEN { res ¬ UnixSysCalls.FSync[fd]; IF res#success AND UnixErrno.GetErrno[] # EBADF THEN { RETURN["FSync failed in Close; file system full?"]; }; }; res ¬ UnixSysCalls.Close[fd]; IF res#success THEN { errno ¬ UnixErrno.GetErrno[]; failure ¬ MakeMsg[selfName, "Close failed for", file]; RETURN[failure, errno]; }; IF data.setNameAtClose#NIL THEN { <> IF NOT abort THEN { res: RES ~ UnixSysCalls.Rename[data.setUniqueIDAtClose, data.setNameAtClose]; IF res#success THEN { errno ¬ UnixErrno.GetErrno[]; failure ¬ MakeMsg[selfName, "Rename failed for", file]; RETURN[failure, errno]; } } ELSE { res: RES ~ UnixSysCalls.Unlink[data.setUniqueIDAtClose]; IF res#success THEN { errno ¬ UnixErrno.GetErrno[]; failure ¬ MakeMsg[selfName, "Delete failed in aborted Close for", file]; RETURN[failure, errno]; } }; }; }; [failure, errno] ¬ DoClose[data]; IF failure # NIL THEN UnixFSPrivate.ReportFailure[errno, failure]; }; UXPFSNameToUnixName: PUBLIC PFSClass.PFSNameToUnixNameProc -- [h: FSHandle, file: PATH] RETURNS [ ROPE ] -- = { RETURN[PFS.RopeFromPath[file]]; }; UXCaseSensitive: PUBLIC PFSClass.CaseSensitiveProc -- [h: FSHandle, file: PATH] RETURNS [ BOOL ] -- = { RETURN[TRUE]; }; MTime: TYPE ~ INT; unixEpoch: BasicTime.GMT ¬ BasicTime.Pack[ [year~1970, month~January, day~1, hour~0, minute~0, second~0, zone~0, dst~no] ]; MTimeFromGMT: PUBLIC PROC [gmt: BasicTime.GMT] RETURNS [unixTime: UnixTypes.Time] ~ { RETURN [BasicTime.Period[from~unixEpoch, to~gmt]]; }; FixUID: PROC [stringName: CString, wantedUniqueID: PFS.UniqueID] ~ TRUSTED { mTime: MTime ~ MTimeFromGMT[wantedUniqueID.egmt.gmt]; modTimeVal: UnixTypes.AccModTimes ¬ [accessTime: [sec: mTime, usec: 0], modTime: [sec: mTime, usec: 0]]; IF wantedUniqueID.egmt.gmt # BasicTime.nullGMT THEN IF UnixSysCalls.UTimes[stringName, @modTimeVal] # success THEN { errno: UnixErrno.Errno ~ UnixErrno.GetErrno[]; UnixFSPrivate.ReportFailure[errno, MakeMsg["FixUID", "UTimes failed for", UXStrings.ToRope[stringName]]]; }; }; <> UIDFromMTime: PROC [mTime: MTime] RETURNS [PFS.UniqueID] ~ { <> RETURN[[egmt: [gmt: HostTime.ExtendedGMTFromHostTime[[a~LOOPHOLE[mTime]]].gmt, usecs: 0], host: [0, 0]]] }; tempNamePrefix: ROPE ¬ NIL; CreateTempName: PROC [base: ROPE] RETURNS [tempName: ROPE] ~ { u1, u2: CARD; IF tempNamePrefix = NIL THEN { a: Arpa.Address ~ Arpa.MyAddress[]; tempNamePrefix ¬ IO.PutFLR["fs.%g.%g.%g.%g", LIST[[cardinal[a.a]], [cardinal[a.b]], [cardinal[a.c]], [cardinal[a.d]]]]; }; u1 ¬ u2 ¬ BasicTime.GetClockPulses[]; u1 ¬ LOOPHOLE[BasicTime.Now[! BasicTime.TimeNotKnown => CONTINUE]]; tempName ¬ IO.PutFR["%g.%g.%g", [rope[tempNamePrefix]], [cardinal[u1]], [cardinal[u2]] ]; }; FixPathForUnix: PROC [path: PATH] RETURNS [fixedPath: PATH] ~ { fixedPath ¬ PFSNames.StripVersionNumber[path]; }; moduleName: ROPE ~ "PFSUXImpl."; -- for producing error messages MakeMsg: PROC [procName: ROPE, constantText: ROPE, file: REF] RETURNS [ROPE] ~ { fileRope: ROPE ~ WITH file SELECT FROM r: ROPE => r, p: PATH => PFS.RopeFromPath[p], o: PFSClass.OpenFile => PFS.RopeFromPath[o.fullFName] ENDCASE => NIL; RETURN[ Rope.Cat[moduleName, procName, ": ", Rope.Cat[constantText, " ", fileRope]] ]; }; jack: PFSNames.Component ~ [["*", 0, 1], [none]]; wild: PATH ~ PFSNames.ConstructName[components: LIST[jack]]; ParsePattern: PROC [pattern: PATH] RETURNS [dirPath, patternPart: PATH] ~ { IF PFSNames.IsADirectory[pattern] THEN { dirPath ¬ pattern; patternPart ¬ wild } ELSE { dirPath ¬ PFSNames.Parent[pattern]; patternPart ¬ FixPathForUnix[pattern.SubName[pattern.ComponentCount[]-1, 1]]; }; }; SortNameList: PROC [list: LIST OF PATH] RETURNS [out: LIST OF PATH] ~ { <> left, right: LIST OF PATH; IF (list = NIL) OR (list.rest = NIL) THEN RETURN[list]; [left, right] ¬ SplitNameList[list]; RETURN[MergeNameLists[SortNameList[left], SortNameList[right]]]; }; SplitNameList: PROC [list: LIST OF PATH] RETURNS [left, right: LIST OF PATH] ~ { <> len: CARDINAL ¬ 0; FOR each: LIST OF PATH ¬ list, each.rest WHILE each # NIL DO len ¬ len + 1; ENDLOOP; IF len = 0 THEN RETURN[NIL, NIL]; left ¬ list; THROUGH [1 .. len/2) DO list ¬ list.rest ENDLOOP; right ¬ list.rest; list.rest ¬ NIL; }; MergeNameLists: PROC [left, right: LIST OF PATH] RETURNS [LIST OF PATH] ~ { <> oFinger, out: LIST OF PATH; out ¬ oFinger ¬ LIST[NIL]; DO SELECT TRUE FROM (left = NIL) => { oFinger.rest ¬ right; EXIT }; (right = NIL) => { oFinger.rest ¬ left; EXIT }; (PFSNames.Compare [left.first, right.first] = less) => { oFinger.rest ¬ left; left ¬ left.rest; }; ENDCASE => { oFinger.rest ¬ right; right ¬ right.rest; }; oFinger ¬ oFinger.rest; oFinger.rest ¬ NIL; ENDLOOP; RETURN[out.rest]; }; <> FailIfReadOnly: PROC [h: PFSClass.FSHandle, file: PATH] ~ { data: FSData ¬ NARROW[h.data]; r: ROPE; IF NOT data.readOnly THEN RETURN; r ¬ Rope.Cat[ "FS for [", IF h # NIL THEN h.name ELSE "?", "]", PFS.RopeFromPath[file], " is read-only."]; PFSBackdoor.ProduceError[accessDenied, r, NIL]; }; <> DInfo: TYPE ~ RECORD [ fileName: PATH, bytes: INT, uniqueID: PFS.UniqueID ]; listOfInfo: LIST OF DInfo ¬ NIL; DoEnumerateForInfo: PROC [h: PFSClass.FSHandle, pattern: PATH] ~ { EachInfo: PFS.InfoProc -- [fullFName, attachedTo, uniqueID, bytes, mutability, fileType] RETURNS [continue: ¬ TRUE] -- ~ { listOfInfo ¬ CONS[[fullFName, bytes, uniqueID], listOfInfo]; RETURN[TRUE]; }; listOfInfo ¬ NIL; UXEnumerateForInfo[h, pattern, EachInfo, NIL, NIL]; }; listOfNames: LIST OF PATH ¬ NIL; DoEnumerateForNames: PROC [h: PFSClass.FSHandle, pattern: PATH] ~ { EachName: PFS.NameProc -- [name: PATH] RETURNS [continue: BOOL ¬ TRUE] -- ~ { listOfNames ¬ CONS[name, listOfNames]; RETURN[TRUE]; }; listOfNames ¬ NIL; UXEnumerateForNames[h, pattern, EachName, NIL, NIL]; }; <> PFSClass.Register[myFlavor, GetHandle]; PFSClass.Register[myReadOnlyFlavor, GetROHandle]; [] ¬ PFSPrefixMap.Insert[ PFS.PathFromRope["/"], uxRoot ]; }. <> <> <<>>