-- FTPPilotFile.mesa -- Edit: HGM, February 27, 1981 2:46 PM Fix for doubling create date -- Edit: HGM, February 4, 1981 11:19 AM -- Edit: BLyon, August 26, 1980 4:04 PM DIRECTORY DCSFileTypes USING [tLeaderPage], Directory USING [ CreateFile, CreateSystemDirectory, DeleteFile, Error, Handle, GetNext, GetProperty, LookupUnlimited], Environment USING [bytesPerPage, bytesPerWord, wordsPerPage], File USING [ Capability, delete, Error, GetAttributes, grow, LimitPermissions, nullCapability, PageCount, Permissions, read, shrink, Type, write], FileStream USING [ Create, GetLeaderProperties, GetLeaderPropertiesForCapability, GetLength, GetIndex, IndexOutOfRange, NoLeaderPage, SetLeaderProperties, SetLength, SetIndex, Subtype], FTPDefs, FTPPrivateDefs, Inline USING [BITOR, LowHalf], PropertyTypes USING [tFileName], Space USING [Create, Delete, Handle, Map, mds, Pointer, Unmap], Stream USING [Block, CompletionCode, Delete, Handle, GetBlock, PutBlock], String USING [ AppendChar, AppendLongNumber, AppendString, AppendSubString, EquivalentString, SubStringDescriptor], System USING [gmtEpoch, GreenwichMeanTime], Storage USING [Node, Free], Time USING [Append, Invalid, Unpack], TimeExtra USING [PackedTimeFromString], Volume USING [ID, systemID, InsufficientSpace]; FTPPilotFile: MONITOR IMPORTS Directory, File, FileStream, Inline, Space, Stream, String, Storage, Time, TimeExtra, Volume, FTPPrivateDefs EXPORTS FTPDefs SHARES FTPDefs, FTPPrivateDefs = BEGIN OPEN FTPDefs, FTPPrivateDefs; -- Note: Absolute filenames should have format as specified by Pilot Directories. -- **********************! Types !*********************** -- pilot file system state information PilotFileSystem: TYPE = POINTER TO PilotFileSystemObject; PilotFileSystemObject: TYPE = RECORD [bufferSize: CARDINAL]; -- pilot file handle state information PilotFileHandle: TYPE = POINTER TO PilotFileHandleObject; PilotFileHandleObject: TYPE = RECORD [ mode: Mode, fileCapability: File.Capability, streamHandle: Stream.Handle]; -- **********************! Constants !*********************** defaultBufferSize: CARDINAL = 4; -- in pages bytesPerPage: CARDINAL = Environment.bytesPerPage; bytesPerWord: CARDINAL = Environment.bytesPerWord; wordsPerPage: CARDINAL = Environment.wordsPerPage; filenameWildString: CHARACTER = '*; filenameWildCharacter: CHARACTER = '#; filenameNameVersionSeparator: CHARACTER = '!; ftpsystem: POINTER TO FTPSystem = LocateFtpSystemObject[]; filePrimitivesObject: FilePrimitivesObject _ [CreateFileSystem: CreateFileSystem, DestroyFileSystem: DestroyFileSystem, DecomposeFilename: DecomposeFilename, ComposeFilename: ComposeFilename, InspectCredentials: InspectCredentials, EnumerateFiles: EnumerateFiles, OpenFile: OpenFile, ReadFile: ReadFile, WriteFile: WriteFile, CloseFile: CloseFile, DeleteFile: DeleteFile, RenameFile: RenameFile]; -- **********************! Variables !*********************** uniqueFileTag: LONG INTEGER _ 0; -- **********************! File Foothold Procedure !*********************** PilotFilePrimitives, SomeFilePrimitives: PUBLIC PROCEDURE RETURNS [filePrimitives: FilePrimitives] = BEGIN -- return file primitives RETURN[@filePrimitivesObject]; END; -- **********************! File Primitives !*********************** CreateFileSystem: PROCEDURE [bufferSize: CARDINAL] RETURNS [fileSystem: FileSystem] = BEGIN -- Note: bufferSize expressed in pages; zero implies default. -- allocate and initialize file system object pilotFS: PilotFileSystem _ Storage.Node[SIZE[PilotFileSystemObject]]; pilotFS.bufferSize _ IF bufferSize # 0 THEN bufferSize ELSE defaultBufferSize; RETURN[LOOPHOLE[pilotFS, FileSystem]]; END; DestroyFileSystem: PROCEDURE [fileSystem: FileSystem] = BEGIN -- release file system object Storage.Free[LOOPHOLE[fileSystem, PilotFileSystem]]; END; DecomposeFilename: PROCEDURE [ fileSystem: FileSystem, absoluteFilename: STRING, virtualFilename: VirtualFilename] = BEGIN OPEN virtualFilename; -- Note: Absolute filenames have the following syntax, -- with name non-empty and version non-empty and numeric: -- name [filenameNameVersionSeparator version] -- We accept file names with version numbers even though the Pilot Directory doesn't -- have this feature. -- Virtual filename components are never NIL, they -- supply empty device and directory components if necessary. i: CARDINAL; character: CHARACTER; field: STRING _ name; -- initialize virtual filename components to empty device.length _ directory.length _ name.length _ version.length _ 0; -- process each character in absolute filename FOR i IN [0..absoluteFilename.length) DO -- select character character _ absoluteFilename[i]; -- switch to version if character is name-version separator IF field = name AND character = filenameNameVersionSeparator THEN field _ version -- append character to name or version as appropriate ELSE BEGIN IF field = version AND character NOT IN ['0..'9] THEN Abort[illegalFilename]; String.AppendChar[field, character]; END; ENDLOOP; -- abort if either name or version is empty IF name.length = 0 OR (field = version AND version.length = 0) THEN Abort[illegalFilename]; END; ComposeFilename: PROCEDURE [ fileSystem: FileSystem, absoluteFilename: STRING, virtualFilename: VirtualFilename] = BEGIN OPEN virtualFilename; -- Note: Absolute filenames have the following syntax, -- with name non-empty and version non-empty and numeric: -- name [filenameNameVersionSeparator version]. -- We accept file names with version numbers even though the Pilot Directory doesn't -- have this feature. -- Virtual filename components are never NIL; -- ignores device and directory components; -- uses name and version components as defaults. -- local constants explicitDevice: STRING = [0]; explicitDirectory: STRING = [0]; -- local variables explicitName: STRING _ [maxStringLength]; explicitVersion: STRING _ [maxStringLength]; explicitVirtualFilenameObject: VirtualFilenameObject _ [device: explicitDevice, directory: explicitDirectory, name: explicitName, version: explicitVersion]; i: CARDINAL; -- return at once if absolute filename is all there is IF name.length = 0 AND version.length = 0 THEN RETURN; -- decompose absolute filename IF absoluteFilename.length # 0 THEN DecomposeFilename[ fileSystem, absoluteFilename, @explicitVirtualFilenameObject]; -- apply defaults as necessary IF explicitName.length = 0 THEN explicitName _ name; IF explicitVersion.length = 0 THEN explicitVersion _ version; -- initialize absolute filename to empty absoluteFilename.length _ 0; -- output name always IF explicitName.length = 0 THEN Abort[illegalFilename]; String.AppendString[absoluteFilename, explicitName]; -- output version if specified IF explicitVersion.length # 0 THEN BEGIN -- verify that version is numeric FOR i IN [0..explicitVersion.length) DO IF explicitVersion[i] NOT IN ['0..'9] THEN Abort[illegalFilename]; ENDLOOP; -- output name-version separator String.AppendChar[absoluteFilename, filenameNameVersionSeparator]; -- output version String.AppendString[absoluteFilename, explicitVersion]; END; END; InspectCredentials: ENTRY PROCEDURE [ fileSystem: FileSystem, status: Status, user, password: STRING] = BEGIN -- no operation END; EnumerateFiles: PROCEDURE [ fileSystem: FileSystem, files: STRING, intent: EnumerateFilesIntent, processFile: PROCEDURE [UNSPECIFIED, STRING, FileInfo], processFileData: UNSPECIFIED] = BEGIN -- Note: Implements filenameWildString and filenameWildCharacter; -- skips directory search if none of either present; supplies no file information (not true for this, Tajo's own, version). -- MaskFilename procedure MaskFilename: PROCEDURE [ file: STRING, fileIndex: CARDINAL, mask: STRING, maskIndex: CARDINAL] RETURNS [outcome: BOOLEAN] = BEGIN -- local variables i, j: CARDINAL; -- process each character in mask FOR i IN [maskIndex..mask.length) DO SELECT mask[i] FROM filenameWildString => -- matches any string of zero or more characters BEGIN FOR j IN [fileIndex..file.length] DO IF MaskFilename[file, j, mask, i + 1] THEN RETURN[TRUE]; ENDLOOP; RETURN[FALSE]; END; filenameWildCharacter => -- matches any single character IF fileIndex = file.length THEN RETURN[FALSE] ELSE fileIndex _ fileIndex + 1; ENDCASE => -- matches itself (ignoring case differences) IF fileIndex = file.length OR Inline.BITOR[ LOOPHOLE[file[fileIndex], UNSPECIFIED], 40B] # Inline.BITOR[ LOOPHOLE[mask[i], UNSPECIFIED], 40B] THEN RETURN[FALSE] ELSE fileIndex _ fileIndex + 1; ENDLOOP; -- filename passes mask if entire filename has been consumed outcome _ fileIndex = file.length; END; -- local variables split: String.SubStringDescriptor; splitName: STRING _ [maxStringLength]; cap: File.Capability; name: STRING _ [maxStringLength]; cStr: STRING _ [maxStringLength]; wStr: STRING _ [maxStringLength]; rStr: STRING _ [maxStringLength]; fileInfoObject: FileInfoObject _ [fileType: unknown, byteSize: 8, byteCount: 0, creationDate: cStr, writeDate: wStr, readDate: rStr, author: NIL]; FillInfo: PROCEDURE = BEGIN OPEN fileInfoObject, Time; cDate, wDate, rDate: System.GreenwichMeanTime _ System.gmtEpoch; subType: FileStream.Subtype _ null; creationDate.length _ writeDate.length _ readDate.length _ 0; [type: subType, create: cDate, write: wDate, read: rDate, length: byteCount] _ FileStream.GetLeaderPropertiesForCapability[ cap ! FileStream.NoLeaderPage => CONTINUE]; fileType _ SELECT subType FROM text => text, byteBinary, wordBinary => binary, ENDCASE => unknown; Append[s: creationDate, unpacked: Unpack[cDate] ! Invalid => CONTINUE]; Append[s: writeDate, unpacked: Unpack[wDate] ! Invalid => CONTINUE]; Append[s: readDate, unpacked: Unpack[rDate] ! Invalid => CONTINUE]; END; name.length _ 0; DO cap _ Directory.GetNext["WorkDir>*"L, name, name]; IF name.length = 0 THEN EXIT; -- no terminating period -- output file if it satisfies the mask -- As a temporary hack, directory qualification is discarded! split _ [base: name, length:, offset:]; FOR i: CARDINAL DECREASING IN [0..name.length) DO IF name[i] = '> THEN {split.offset _ i + 1; EXIT}; ENDLOOP; split.length _ name.length - split.offset; splitName.length _ 0; String.AppendSubString[splitName, @split]; IF MaskFilename[splitName, 0, files, 0] THEN { FillInfo[]; processFile[processFileData, splitName, @fileInfoObject]}; ENDLOOP; END; OpenFile: ENTRY PROCEDURE [ fileSystem: FileSystem, file: STRING, mode: Mode, fileTypePlease: BOOLEAN, info: FileInfo] RETURNS [fileHandle: FileHandle, fileType: FileType] = BEGIN ENABLE UNWIND => NULL; -- Note: Supplies scratch filename if file.length=0. -- local constants existingFileRequired: BOOLEAN = (mode = read OR mode = append OR mode = readThenWrite); permissions: File.Permissions = SELECT mode FROM read => File.read, append => File.read + File.write + File.grow + File.shrink, ENDCASE => File.read + File.write + File.grow + File.shrink + File.delete; -- write, writeThenRead, readThenWrite -- local variables successful: BOOLEAN; cap: File.Capability _ File.nullCapability; pilotFileType: File.Type; goodInitialFileSize: File.PageCount = 100; pilotFH: PilotFileHandle _ NIL; fileSubtype: FileStream.Subtype; leaderFileName: STRING _ [maxStringLength]; createDate: System.GreenwichMeanTime; -- generate unique scratch filename if necessary IF file.length = 0 THEN BEGIN String.AppendString[file, "FTPPilotFile-"L]; String.AppendLongNumber[file, UniqueFileTag[], 10]; String.AppendString[file, ".Scratch"L]; END; -- catch erorrs BEGIN ENABLE BEGIN UNWIND => BEGIN IF pilotFH # NIL THEN Storage.Free[pilotFH]; END; Directory.Error => IF type = invalidFileName THEN AbortWithExplanation[requestedAccessDenied, file]; END; -- find this file in the directory successful _ TRUE; cap _ Directory.LookupUnlimited[ fileName: file ! Directory.Error => IF type = fileNotFound THEN {successful _ FALSE; CONTINUE}]; IF NOT successful THEN BEGIN IF existingFileRequired THEN Abort[noSuchFile] ELSE BEGIN cap _ Directory.CreateFile[file, DCSFileTypes.tLeaderPage, goodInitialFileSize]; END; END; -- initialize pilot file handle object pilotFH _ Storage.Node[SIZE[PilotFileHandleObject]]; fileHandle _ LOOPHOLE[pilotFH, FileHandle]; pilotFH.mode _ mode; pilotFH.fileCapability _ File.LimitPermissions[cap, permissions]; pilotFH.streamHandle _ FileStream.Create[pilotFH.fileCapability]; [pilotFileType, , , ] _ File.GetAttributes[pilotFH.fileCapability]; IF pilotFileType = DCSFileTypes.tLeaderPage THEN [type: fileSubtype, create: createDate] _ FileStream.GetLeaderProperties[ pilotFH.streamHandle, leaderFileName]; -- determine file type if necessary fileType _ SELECT TRUE FROM ~fileTypePlease OR pilotFileType # DCSFileTypes.tLeaderPage => unknown, fileSubtype = text => text, fileSubtype = byteBinary => binary, ENDCASE => unknown; -- process the info IF info # NIL THEN BEGIN IF mode = write AND info.creationDate # NIL AND pilotFileType = DCSFileTypes.tLeaderPage THEN FileStream.SetLeaderProperties[ sH: pilotFH.streamHandle, create: TimeExtra.PackedTimeFromString[info.creationDate]]; IF (mode = read OR mode = readThenWrite) AND info.creationDate # NIL AND info.creationDate.length = 0 -- Filled in by Enumerate AND pilotFileType = DCSFileTypes.tLeaderPage THEN Time.Append[info.creationDate, Time.Unpack[createDate]]; END; END; -- enable END; -- This procedure reads the entire contents of a file when it is called. ReadFile: ENTRY PROCEDURE [ fileSystem: FileSystem, fileHandle: FileHandle, sendBlock: PROCEDURE [UNSPECIFIED, POINTER, CARDINAL], sendBlockData: UNSPECIFIED] = BEGIN OPEN pilotFH: LOOPHOLE[fileHandle, PilotFileHandle]; ENABLE UNWIND => NULL; -- Note: Assumes invocation is consistent with mode declared via OpenFile. -- local variables location: POINTER; pilotFS: PilotFileSystem = LOOPHOLE[fileSystem]; bufferByteCount: CARDINAL = pilotFS.bufferSize*bytesPerPage; block: Stream.Block; bytesTransferred: CARDINAL; why: Stream.CompletionCode _ normal; -- get a scratch space spaceH: Space.Handle _ Space.Create[pilotFS.bufferSize, Space.mds]; Space.Map[spaceH]; location _ Space.Pointer[spaceH]; block _ [blockPointer: location, startIndex: 0, stopIndexPlusOne: bufferByteCount]; -- set the byte index to the beginning of the file FileStream.SetIndex[pilotFH.streamHandle, 0]; -- read file UNTIL why = endOfStream DO [bytesTransferred, why, ] _ Stream.GetBlock[ pilotFH.streamHandle, block ! FileStream.IndexOutOfRange => EXIT]; -- this signal can only be generated if the file was empty to start with, -- otherwise we would have detected end-of-file and exited. -- give the block of data to the caller sendBlock[sendBlockData, location, bytesTransferred]; ENDLOOP; -- tell client that there is no more. sendBlock[sendBlockData, NIL, 0]; -- get rid of the scratch space Space.Unmap[spaceH]; Space.Delete[spaceH]; END; WriteFile: ENTRY PROCEDURE [ fileSystem: FileSystem, fileHandle: FileHandle, receiveBlock: PROCEDURE [UNSPECIFIED, POINTER, CARDINAL] RETURNS [CARDINAL], receiveBlockData: UNSPECIFIED] = BEGIN OPEN pilotFH: LOOPHOLE[fileHandle, PilotFileHandle]; ENABLE UNWIND => NULL; -- Note: Assumes invocation is consistent with mode declared via OpenFile. pilotFS: PilotFileSystem = LOOPHOLE[fileSystem]; destination: POINTER; bufferWordCount: CARDINAL = pilotFS.bufferSize*wordsPerPage; length: CARDINAL; block: Stream.Block; flushFile: BOOLEAN _ FALSE; -- get a scratch space spaceH: Space.Handle _ Space.Create[pilotFS.bufferSize, Space.mds]; Space.Map[spaceH]; destination _ Space.Pointer[spaceH]; -- set the byte index according to the mode FileStream.SetIndex[ pilotFH.streamHandle, IF pilotFH.mode = append THEN FileStream.GetLength[pilotFH.streamHandle] ELSE 0]; -- write file UNTIL (length _ receiveBlock[receiveBlockData, destination, bufferWordCount]) = 0 DO block _ [blockPointer: destination, startIndex: 0, stopIndexPlusOne: length]; IF ~flushFile THEN Stream.PutBlock[ pilotFH.streamHandle, block, FALSE ! Volume.InsufficientSpace => BEGIN flushFile _ TRUE; CONTINUE; END]; ENDLOOP; -- truncate the byte length of the file if the file previously existed and is now smaller. -- The file stream package does not truncate the byte length for us. FileStream.SetLength[ pilotFH.streamHandle, FileStream.GetIndex[pilotFH.streamHandle]]; -- get rid of the scratch space Space.Unmap[spaceH]; Space.Delete[spaceH]; IF flushFile THEN Abort[noRoomForFile]; END; CloseFile: ENTRY PROCEDURE [ fileSystem: FileSystem, fileHandle: FileHandle, aborted: BOOLEAN] = BEGIN OPEN pilotFH: LOOPHOLE[fileHandle, PilotFileHandle]; ENABLE UNWIND => NULL; -- Note: On abort, deletes file opened for write, writeThenRead, or readThenWrite. -- local variables fileName: STRING _ [maxStringLength]; fileNameL: LONG STRING _ fileName; fileNameDesc: LONG DESCRIPTOR FOR ARRAY OF UNSPECIFIED _ DESCRIPTOR[ fileNameL, SIZE[StringBody [maxStringLength]]]; Stream.Delete[pilotFH.streamHandle]; -- close the file stream, may generate a SIGNAL -- delete file if appropriate IF aborted AND (pilotFH.mode = write OR pilotFH.mode = writeThenRead OR pilotFH.mode = readThenWrite) THEN BEGIN -- delete file from directory. -- We do not catch any Signals or Errors since we are not doing anything tricky, -- and assume that since we opened the file, the capability and name are correct. Directory.GetProperty[ pilotFH.fileCapability, PropertyTypes.tFileName, fileNameDesc]; Directory.DeleteFile[fileName]; END; -- release pilot file handle object Storage.Free[fileHandle]; END; DeleteFile: ENTRY PROCEDURE [fileSystem: FileSystem, file: STRING] = BEGIN ENABLE BEGIN UNWIND => NULL; Directory.Error => IF type = invalidFileName THEN AbortWithExplanation[requestedAccessDenied, file] ELSE IF type = directoryNeedsScavenging THEN Abort[requestedAccessDenied]; File.Error => IF type = immutable OR type = insufficientPermissions THEN Abort[requestedAccessDenied]; END; -- local variables -- delete file entry from directory Directory.DeleteFile[ file ! Directory.Error => IF type = fileNotFound THEN Abort[noSuchFile]]; END; RenameFile: ENTRY PROCEDURE [ fileSystem: FileSystem, currentFile, newFile: STRING] = BEGIN ENABLE UNWIND => NULL; -- local variables create: STRING = [maxDateLength]; author: STRING = [maxDateLength]; fileInfo: FileInfoObject _ [binary, 8, 0, create, NIL, NIL, author]; currentFileHandle, newFileHandle: FileHandle _ NIL; -- no operation if two filenames equivalent IF String.EquivalentString[currentFile, newFile] THEN RETURN; -- open current and new files [currentFileHandle, ] _ OpenFile[ fileSystem, currentFile, readThenWrite, FALSE, @fileInfo]; BEGIN ENABLE UNWIND => BEGIN IF newFileHandle # NIL THEN CloseFile[fileSystem, newFileHandle, TRUE]; CloseFile[fileSystem, currentFileHandle, FALSE]; END; [newFileHandle, ] _ OpenFile[fileSystem, newFile, write, FALSE, @fileInfo]; -- transfer contents of current file to new file ForkTransferPair[ fileSystem, ReadFile, currentFileHandle, WriteFile, newFileHandle]; -- close current and new files CloseFile[fileSystem, newFileHandle, FALSE]; END; -- enable -- Note: aborted=TRUE, requesting deletion of current file. CloseFile[fileSystem, currentFileHandle, TRUE]; END; -- **********************! Subroutines !*********************** UniqueFileTag: INTERNAL PROCEDURE RETURNS [LONG INTEGER] = BEGIN -- generate and return unique file tag; problem if wrap around RETURN[uniqueFileTag _ uniqueFileTag + 1]; END; Shorten: PROCEDURE [long: LONG INTEGER] RETURNS [CARDINAL] = INLINE BEGIN RETURN[LOOPHOLE[Inline.LowHalf[long], CARDINAL]]; END; -- **********************! Main Program !*********************** Directory.CreateSystemDirectory[ Volume.systemID ! Directory.Error => IF type = fileAlreadyExists THEN CONTINUE]; END. -- of FTPPilotFile LOG Time: September 13, 1979 9:21 AM By: Dalal Action: conversion to Pilot 3.0. Time: September 25, 1979 9:25 AM By: Dalal Action: fixed WriteFile bug. Time: February 10, 1980 12:23 AM By: Forrest Action: Changed 0's in xxLeaderPagexx to [0]'s. Time: August 1, 1980 2:03 PM By: Marzullo Action: conversion to new directory interface Time: August 6, 1980 11:55 AM By: Evans Action: Changed EnumerateFiles to non-Entry procedure Time: August 9, 1980 10:26 PM By: Sapsford Action: fix bug in use of Directory.GetNext in Enumerate. Bruce, Richard, and Keith copied Evans FileInfo fixes earlier today. Time: August 9, 1980 10:26 PM By: Bruce Action: replaced EmptyFileInfoObject usage with code from FTPAltoFile since semantics changed. Time: August 26, 1980 4:05 PM By: BLyon Action: renamed this file