// Dirs.Bcpl -- versionless directory code
// Copyright Xerox Corporation 1979, 1980, 1981
// Last modified October 27, 1981  2:55 PM by Boggs

get "AltoFileSys.d"
get "Disks.d"
get "Streams.d"

external
[
// outgoing procedures
FindFdEntry; MakeNewFdEntry; DeleteFdEntry; ParseFileName
DeleteFile; OpenFile; OpenFileFromFp; SetWorkingDir
StripVersion; AppendVersion

// incoming procedures
ScanDirBuffer; InitScanStream; GetScanStreamBuffer; FinishScanStream
Gets; Puts; Resets; Errors; Closes
WriteBlock; SetFilePos
CreateDiskStream; GetCompleteFa; LnPageSize; JumpToFa
Allocate; Free; SysErr
CreateDiskFile; DeleteDiskPages; ActOnDiskPages
MoveBlock; Zero; Noop; DefaultArgs; Dvec; Min

// outgoing static
dirVersions

// incoming statics
sysZone; sysDisk
]

static [ dirVersions = false ]

manifest
[
// error codes
ecDeTooBig = 1501
ecBadDeType = 1502
ecBadDirName = 1503
ecFnTooLong = 1504
ecCantOpenFile = 1505

maxExtraSpace = lSTRING
maxDeSize = lDV+maxLengthFnInWords+maxExtraSpace
maxFreeDeSize = (1 lshift size DV.length)-1

fnMask = not (40b*(400b+1)) //137,,137 in case you didn't guess...

// version control masks (see definitions of verxxx in streams.d)
verCrMask = 40000B	// if non-zero, create file ok
]

structure HD:  // Hole Descriptor
[
maxSize word
neededSize word
pos word
]
compileif size HD/16 ne 3 then [ Error("Change lHD in Streams.d") ]

structure SDB:	// ScanDirBuffer state -- must agree with DirScanA.asm
[
name word	// -> name being looked up (all upper-case)
length word	// number of words to check in name
ignoreFree word	// true to skip over free entries, false to stop on each one
scratch word 5	// scratch region used by ScanDirBuffer procedure
]
manifest lenSDB = size SDB/16

structure FSS:  // File Scan State
[
buffer word	// -> current buffer (0 => hit end-of-file)
pos word	// word position in buffer of current directory entry
endPos word	// word position of first word not in buffer
basePos word	// file word position of first word of buffer
ssd word	// -> Scan Stream Descriptor
]

//---------------------------------------------------------------------------
let FindFdEntry(dirS, name, compareFn, dv, hd, nil,
    extraSpace; numargs na) = valof
//---------------------------------------------------------------------------
// FindFdEntry returns the position, or -1 if the name is not found.
// The name is not parsed so this should have already been done.
// If dv is supplied, the dv of the name we find is returned in it.
// If hd is supplied, it is filled in with a hole descriptor for a hole big
//  enough to hold an entry for name with extraSpace extra words of space.
// If compareFn is supplied, it should return 0 if the entry should be
//  accepted.
[
let extraBuf = 1 lshift LnPageSize(dirS)
Dvec(FindFdEntry, lv extraBuf)
let d, h = vec lDV, vec lHD
DefaultArgs(lv na, -2, 0, d, h, nil, 0)

Zero(hd, lHD)
hd>>HD.neededSize = extraSpace

let lNameInWords = nil
let sdb = vec lenSDB
sdb>>SDB.ignoreFree = false
let capName = vec maxLengthFnInWords

if compareFn eq 0 then
   [
   // the +2 adds the length byte and rounds up
   let t = name>>STRING.length + 2
   // the -1 takes off the trailing $.
   lNameInWords = (t-1) rshift 1
   hd>>HD.neededSize = Min(t rshift 1 + lDV + extraSpace, maxDeSize)

   // Set up ScanDirBuffer state and generate capitalized version of name
   sdb>>SDB.name = capName
   sdb>>SDB.length = lNameInWords
   capName!0 = name!0 & 177737B
   for i = 1 to lNameInWords-1 do capName!i = name!i & fnMask
   ]

let tempDV = vec maxDeSize
let bestPos = -1
let holeFA = vec lFA; holeFA>>FA.charPos = 0

// The following must be declared in the order defined in the FSS structure
let buffer, pos, endPos, basePos, ssd = 0, 0, 0, 0, nil
let fss = lv buffer

Resets(dirS)
ssd = InitScanStream(dirS, lv extraBuf, 1)
AdvanceBuffer(fss)

while buffer ne 0 & pos ls endPos do
   [
   // Advance the pointer to the first "interesting" directory entry.
   // The call to ScanDirBuffer accelerates the search, but if the call is
   // omitted the search still works, only more slowly.
   if compareFn eq 0 then
      pos = ScanDirBuffer(buffer+pos, buffer+endPos, sdb) - buffer

   // Now inspect the entry carefully.
   let filePos = basePos + pos
   let thisDV = buffer + pos
   let len = thisDV>>DV.length
   switchon thisDV>>DV.type into
      [
      case dvTypeFree:
         [
         // note that we never accumulate a sequence of free blocks longer
         // than neededSize+(size of biggest free block)
         if sdb>>SDB.ignoreFree then endcase  // already found hole
         if hd>>HD.pos+hd>>HD.maxSize ne filePos then
            [ // Not adjacent to previous hole; record beginning of new one
            hd>>HD.maxSize = 0
            hd>>HD.pos = filePos
            holeFA>>FA.da = ssd>>SSD.da
            holeFA>>FA.pageNumber = ssd>>SSD.pageNumber
            ]
         hd>>HD.maxSize = hd>>HD.maxSize+len
         sdb>>SDB.ignoreFree = hd>>HD.maxSize ge hd>>HD.neededSize
         endcase
         ]
      case dvTypeFile:
         [
         // keep a malformed directory entry from overflowing thisName
         if len gr maxDeSize then Errors(dirS, ecDeTooBig, filePos)
         MoveBlock(tempDV, thisDV, len)
         pos = pos+len
         if pos ge endPos then
            [ AdvanceBuffer(fss); MoveBlock(tempDV+len-pos, buffer, pos) ]
         test compareFn eq 0
            ifso
               [  //default system compareFn
               tempDV!len = 1  // fake next block of length 1 to stop scan
               unless ScanDirBuffer(tempDV, tempDV+len+1, sdb) eq tempDV loop
               ]
            ifnot unless compareFn(name, tempDV+lDV, tempDV) eq 0 loop

         // If we get here, the desired entry was found
         MoveBlock(dv, tempDV, lDV)
         bestPos = filePos
         break
         ]
      default:  // unknown type of directory entry
         [
         Errors(dirS, ecBadDeType, filePos)
         endcase
         ]
      ]
   pos = pos+len
   while pos ge endPos & buffer ne 0 do AdvanceBuffer(fss)
   ]

FinishScanStream(ssd)

// If we found a hole and did not find a matching directory entry, position
// the stream to the page containing the hole.
// If we did not find a hole, set the HD.pos to end-of-file.
test sdb>>SDB.ignoreFree
   ifso if bestPos eq -1 & hd ne h then JumpToFa(dirS, holeFA)
   ifnot hd>>HD.pos = basePos+pos

resultis bestPos
]

//---------------------------------------------------------------------------
and AdvanceBuffer(fss) be
//---------------------------------------------------------------------------
[
fss>>FSS.buffer = GetScanStreamBuffer(fss>>FSS.ssd)
fss>>FSS.basePos = fss>>FSS.basePos + fss>>FSS.endPos
fss>>FSS.pos = fss>>FSS.pos - fss>>FSS.endPos
fss>>FSS.endPos = fss>>FSS.ssd>>SSD.numChars rshift 1
]

//---------------------------------------------------------------------------
and MakeNewFdEntry(dirS, name, dv, hd, extraStuff) be
//---------------------------------------------------------------------------
// Make an entry (name, dv) of size hd>>HD.neededSize in dirS at the hole
// specified by hd.  This hole is of size hd>>HD.maxSize, which is either
// bigger than hd>>HD.neededSize or at the end of dirS.  The hd's
// maxSize-neededSize must not be greater than maxFreeDeSize; hd's
// produced by FindFdEntry have this property, since they are obtained by
// concatenating free de's until a big enough hole is obtained.
// The name should be parsed by the caller.
[
let lNameInWords = name>>STRING.length rshift 1 +1
if lNameInWords gr maxLengthFnInWords then Errors(dirS, ecFnTooLong, name)
dv>>DV.type = dvTypeFile
dv>>DV.length = hd>>HD.neededSize
SetFilePos(dirS, 0, 2*(hd>>HD.pos))
WriteBlock(dirS, dv, lDV)
WriteBlock(dirS, name, lNameInWords)
WriteBlock(dirS, extraStuff, hd>>HD.neededSize-lDV-lNameInWords)

let extra = hd>>HD.maxSize-hd>>HD.neededSize
if extra gr 0 then  //make remaining words into a free block
   [
   let h = nil
   h<<DV.type = dvTypeFree
   h<<DV.length = extra
   Puts(dirS, h)
   ]
]

//---------------------------------------------------------------------------
and DeleteFdEntry(dirS, pos) be
//---------------------------------------------------------------------------
[
SetFilePos(dirS, 0, 2*pos)
let h = Gets(dirS)
h<<DV.type = dvTypeFree
SetFilePos(dirS, 0, 2*pos)
Puts(dirS, h)
]

//---------------------------------------------------------------------------
and ParseFileName(fn, n, list, versionControl) = valof
//---------------------------------------------------------------------------
// Parses n into fn, appending a $. if necessary
// Returns a directory stream in which to look for the name.
// list!0 = errRtn; list!1 = zone; list!3 = disk
[
let length = n>>STRING.length; let sep = 0
for i = 1 to length do
   [
   let c = n>>STRING.char↑i
   if c eq $< % c eq $> then sep = i
   ]
let dirFn = vec maxLengthFnInWords
ExtractLegalFileName(n, dirFn, 0, sep-1)
ExtractLegalFileName(n, fn, sep, length)

// Now need to check to see if dirFn is null (in which case use WorkingDir)
// or <, in which case use SysDir
let fp = 0
let disk = list!3
if dirFn>>STRING.length eq 0 then
   [
   // Assume working directory:
   fp = disk>>DSK.fpWorkingDir
   dirFn = disk>>DSK.nameWorkingDir
   if n>>STRING.char↑sep eq $< then [ fp = disk>>DSK.fpSysDir; dirFn = 0 ]
   ]
resultis OpenFile(dirFn, 0, 0, versionControl, fp, list!0, list!1,
 nil, disk, 0, 100000b)
]

//---------------------------------------------------------------------------
and ExtractLegalFileName(srcS, destS, firstMinus1, last) be
//---------------------------------------------------------------------------
[
let length = last - firstMinus1
Zero(destS, (length+3) rshift 1)  // in particular, zero length & garbage byte
if length gr 0 then
   [
   for i = 1 to length do  //make sure each character is legal
      [
      let char = srcS>>STRING.char↑(firstMinus1+i)
      switchon char into
         [
         default:
            unless ((char&137B) ge $A & (char&137B) le $Z) %
             (char ge $0 & char le $9) do char = $-
         case $-: case $$: case $!:
         case $+: case $.: case $<: case $>:
         ]
      destS>>STRING.char↑i = char
      ]
   destS>>STRING.length = length
   StripVersion(destS)  // append $. if necessary
   ]
]

//---------------------------------------------------------------------------
and SetWorkingDir(name, fp, disk; numargs na) be
//---------------------------------------------------------------------------
[
if na ls 3 then disk = sysDisk
MoveBlock(disk>>DSK.fpWorkingDir, fp, lFP)
MoveBlock(disk>>DSK.nameWorkingDir, name, maxLengthFnInWords)
]

//---------------------------------------------------------------------------
and OpenFile(name, ksType, itemSize, versionControl, hintFp, errRtn,
 zone, nil, disk, CreateStream, SNword; numargs na) = valof
//---------------------------------------------------------------------------
[
let defaultFp = vec lFP
DefaultArgs(lv na, -1, ksTypeReadWrite, wordItem, 0, defaultFp, SysErr,
 sysZone, nil, sysDisk, CreateDiskStream, 0)
if versionControl eq 0 then
   versionControl = ksType eq ksTypeReadOnly? verLatest, verLatestCreate

   [ // repeat
   // Following check tries to decide whether the hint is filled in.
   // It may be that user just wants it filled in.
   // On the second iteration it will always call CreateStream.
   if hintFp ne defaultFp & hintFp>>FP.version ne 0 then
      [
      let s = CreateStream(hintFp, ksType, itemSize, Noop, errRtn,
       zone, nil, disk)
      if s ne 0 resultis s
      ]

   if defaultFp eq 0 resultis errRtn(nil, ecCantOpenFile)
   defaultFp = 0  // Force CreateStream to be done on next iteration

   // blunder check
   if name eq 0 % name>>STRING.length eq 0 resultis 0

   let fixedName, dv, hd = vec maxLengthFnInWords, vec lDV, vec lHD

   // strip off the directory info, return a name body and dir stream
   let currentDirS = ParseFileName(fixedName, name, lv errRtn, versionControl)
   if currentDirS eq 0 resultis 0  //no such directory

   // go look in the directory for the file.
   if FindFdEntry(currentDirS, fixedName, 0, dv, hd) eq -1 then
      [ // Not there; allowed to create?
      if (versionControl & verCrMask) eq 0 then
         [ Closes(currentDirS); resultis 0 ]
      let dirCfa = vec lCFA; GetCompleteFa(currentDirS, dirCfa)
      CreateDiskFile(disk, fixedName, lv dv>>DV.fp, lv dirCfa>>CFA.fp, SNword)
      MakeNewFdEntry(currentDirS, fixedName, dv, hd)
      ]

   Closes(currentDirS)
   MoveBlock(hintFp, lv dv>>DV.fp, lFP)
   ] repeat
]

//---------------------------------------------------------------------------
and OpenFileFromFp(fp) = OpenFile(0, 0, 0, 0, fp)
//---------------------------------------------------------------------------

//---------------------------------------------------------------------------
and DeleteFile(name, nil, errRtn, zone, nil, disk; numargs n) = valof
//---------------------------------------------------------------------------
// returns false if it couldn't find the file
[
DefaultArgs(lv n, -1, verOldest, SysErr, sysZone, nil, sysDisk)
let fixedName = vec maxLengthFnInWords
let dv = vec lDV
let currentDirS = ParseFileName(fixedName, name, lv errRtn, verLatest)
if currentDirS eq 0 resultis false  //bad directory name
let pos = FindFdEntry(currentDirS, fixedName, 0, dv)
if pos eq -1 then [ Closes(currentDirS); resultis false ]
DeleteFdEntry(currentDirS, pos)
Closes(currentDirS)

let buf = Allocate(zone, 1 lshift (disk>>DSK.lnPageSize))

// Need to read the leader page in order to get the last page hint.
// This costs an extra revolution, but will usually be much less costly
// than letting the disk seek to cylinder 0 when it reaches end-of-file
// during the delete.
ActOnDiskPages(disk, lv buf, lv dv>>DV.fp.leaderVirtualDa, lv dv>>DV.fp,
 0, 0, DCreadD)

// Delete all pages of the file, starting with page 0
DeleteDiskPages(disk, buf, dv>>DV.fp.leaderVirtualDa, lv dv>>DV.fp,
 0, 0, buf>>LD.hintLastPageFa.pageNumber)
Free(zone, buf)
resultis true
]

//---------------------------------------------------------------------------
and StripVersion(fn, lvVersionExists; numargs na) = valof
//---------------------------------------------------------------------------
[
if na gr 1 then @lvVersionExists = false
let len = fn>>STRING.length
if fn>>STRING.char↑len ne $. then
   [
   fn>>STRING.length = len +1
   fn>>STRING.char↑(len+1) = $.
   ]
resultis 0
]

//---------------------------------------------------------------------------
and AppendVersion(nil, nil) be [ ]
//---------------------------------------------------------------------------