// ScavDirs.bcpl -- Reconstruct Directories
// Copyright Xerox Corporation 1979, 1980, 1981, 1982
// Last modified September 29, 1982  1:32 AM by Boggs

get "Streams.d"
get "AltoFileSys.d"
get "Disks.d"
get "BFS.d"
get "Scavenger.decl"

external
[
// outgoing procedures
FixDirs

// incoming procedures
GetBit; SetBit; GetString; XferPage; Ws; CopyString
CreateD; ForAllFDs; FindDShape; WouldHave; CheckedVDA

FindFdEntry; MakeNewFdEntry
FileLength; FilePos; SetFilePos
ReadBlock; WriteBlock; ReadLeaderPage
CreateDiskStream; TruncateDiskStream
RealDiskDA; VirtualDiskDA; AssignDiskPage
MoveBlock; Zero; DoubleAdd; DoubleUsc
Allocate; Free; Enqueue; Dequeue
PutTemplate; Closes; Gets; Puts

// incoming statics
sysZone; sysDisk; dsp
alterFlag; label; data
logFlag; keys; maxVDA
badBT; usedBT; pt; fdbQ
banzaiFlag
]

manifest maxLenDV = lDV+maxLengthFnInWords+128  //from dirs.bcpl

static
[
dirFD		// fd of current directory
dirName		// string name of current directory
dirSt		// stream open on current directory
newDvQ		// queue of DVs to add to current directory
]

structure String [ length byte; char↑1,1 byte ]

//-----------------------------------------------------------------------------------------
let FixDirs() be
//-----------------------------------------------------------------------------------------
[
// Make sure SysDir exists
GuaranteeSysDir()

// Verify and compact directories
ForAllFDs(CheckDir)

// Insert lost files
ForAllFDs(InsertFile)

CloseDir()

while fdbQ!0 ne 0 do Free(sysZone, Dequeue(fdbQ))
Free(sysZone, fdbQ)
]

//-----------------------------------------------------------------------------------------
and GuaranteeSysDir() be
//-----------------------------------------------------------------------------------------
[
let fd = FindFile(1)
if fd ne 0 & fd>>FD.state eq fdGood &
 fd>>FD.sn.word1 eq 100000b & fd>>FD.sn.word2 eq 144b return

// main directory is lost!
unless alterFlag do
   [
   Ws("*NThe main directory (SysDir) is lost.")
   Ws("*NI cannot proceed without altering your disk.")
   Ws("*NType any key to finish.")
   Gets(keys)
   finish
   ]

// rip off page 1 of disk
test GetBit(badBT, 1)
   ifso SetBit(badBT, 1, false)
   ifnot test GetBit(usedBT, 1)
      ifnot SetBit(usedBT, 1, true)
      ifso
         [
         Ws("*NPage 1 of the disk was ripped off to make SysDir.")
         Ws("*NScavenge again to recover the damaged file.")
         ]

// find a second page for SysDir
let page1 = AssignDiskPage(sysDisk, 1)

// Write page 0 of SysDir
Zero(data, 256)
CopyString(lv data>>LD.name, "SysDir.")
label>>DL.fileId.serialNumber.word1 = 100000b
label>>DL.fileId.serialNumber.word2 = 144b
label>>DL.fileId.version = 1
label>>DL.pageNumber = 0
label>>DL.numChars = 512
label>>DL.previous = 0
RealDiskDA(sysDisk, page1, lv label>>DL.next)
XferPage(DCwriteLD, 1)

// Write page 1 of SysDir
label>>DL.pageNumber = 1
label>>DL.numChars = 0
RealDiskDA(sysDisk, 1, lv label>>DL.previous)
label>>DL.next = 0
XferPage(DCwriteLD, page1)

// Enter in FD list so ForAllFDs will generate it
if fd eq 0 then fd = CreateD(fdbQ)
fd>>FD.sn.word1 = 100000b
fd>>FD.sn.word2 = 144b
fd>>FD.firstVDA = 1
fd>>FD.state = fdGood
]

//-----------------------------------------------------------------------------------------
and CheckDir(fd, nil) be
//-----------------------------------------------------------------------------------------
// Verify and compact a directory
[
unless fd>>FD.sn.directory & fd>>FD.state eq fdGood return  //not a directory file
OpenDir(fd)
let q = vec 1; q!0 = 0; newDvQ = q

let maxFiles = maxVDA/11
let heap, buffer = pt, pt+maxFiles
let readPos = vec 1; FilePos(dirSt, readPos)
let writePos = vec 1; FilePos(dirSt, writePos)
let mangledDir = false; until mangledDir do
   [
   SetFilePos(dirSt, readPos)
   let nWords = ReadBlock(dirSt, buffer, 10*maxFiles); if nWords eq 0 break
   let nFiles, inDV = 0, buffer
   while inDV uls buffer+nWords & nFiles ls maxFiles do
      [
      let type, length = inDV>>DV.type, inDV>>DV.length
      if length eq 0 % length gr maxLenDV %
       (type ne dvTypeFile & type ne dvTypeFree) then
         [
         PutTemplate(dsp, "*NStrange entry in directory $S.", dirName)
         mangledDir = true
         logFlag = true
         break
         ]
      if inDV+length ugr buffer+nWords break  //dv overflows buffer
      if type eq dvTypeFile then
         [
         heap!nFiles = inDV
         nFiles = nFiles +1
         ]
      inDV = inDV + length  //advance to next dv
      ]
   let len = vec 1; len!0 = 0; len!1 = (inDV-buffer) lshift 1
   DoubleAdd(readPos, len)

   let l, r = nFiles rshift 1, nFiles-1
   if nFiles gr 1 then  //heap sort on leader VDA
      [
      let dv = nil
      test l gr 0
         ifso [ l = l -1; dv = heap!l ]
         ifnot
            [
            dv = heap!r; heap!r = heap!0
            r = r -1
            if r eq 0 then [ heap!0 = dv; break ]
            ]
      let j, i = l, nil
         [
         i = j
         j = j lshift 1 +1
         if j gr r break
         if j ls r then
            if (heap!j)>>DV.fp.leaderVirtualDa uls (heap!(j+1))>>DV.fp.leaderVirtualDa then j = j+1
         if (heap!j)>>DV.fp.leaderVirtualDa uls dv>>DV.fp.leaderVirtualDa break
         heap!i = heap!j
         ] repeat
      heap!i = dv
      ] repeat
   for i = 0 to nFiles-1 do CheckDV(heap!i)

// CheckDir (cont'd)

   SetFilePos(dirSt, writePos)
   let outDV = buffer; while outDV uls inDV do
      [
      if outDV>>DV.type eq dvTypeFile then
         if alterFlag then WriteBlock(dirSt, outDV, outDV>>DV.length)
      outDV = outDV + outDV>>DV.length
      ]
   FilePos(dirSt, writePos)
   ]

// append any new DVs created by CheckDV
if newDvQ!0 ne 0 then
   [
   SetFilePos(dirSt, writePos)
      [
      let newDV = Dequeue(newDvQ); if newDV eq 0 break
      WriteBlock(dirSt, newDV+1, (newDV+1)>>DV.length)
      Free(sysZone, newDV)
      ] repeat
   FilePos(dirSt, writePos)
   ]  // end of "until mangledDir do" loop

// Fill out file with free entries
let fileLength = vec 1; FileLength(dirSt, fileLength)
SetFilePos(dirSt, writePos)
Zero(buffer, 100)
buffer>>DV.type = dvTypeFree
buffer>>DV.length = 100
if alterFlag then
   [
   let left = vec 1; MoveBlock(left, fileLength, 2)
   let pos = vec 1; FilePos(dirSt, pos)
   pos!0 = not pos!0; pos!1 = not pos!1
   DoubleAdd(pos, table [ 0; 1 ])
   DoubleAdd(left, pos)	//DoubleSubtract(left, pos)
   if DoubleUsc(left, table [ 0; 1 ]) le 0 break
   if DoubleUsc(left, table [ 0; 200 ]) ls 0 then
      buffer>>DV.length = left!1 rshift 1
   WriteBlock(dirSt, buffer, buffer>>DV.length)
   ] repeat

if alterFlag then TruncateDiskStream(dirSt)  //possible odd byte
]

//-----------------------------------------------------------------------------------------
and CheckDV(dv) be
//-----------------------------------------------------------------------------------------
[
let name, newDV = lv dv>>DV.name, 0
if alterFlag then unless LegalName(name) do
   name = GetName("*NFilename *"$S*" has bad syntax.", name)
if name ne lv dv>>DV.name then
   [
   let length = lDV + name>>String.length lshift 1 +1
   newDV = Allocate(sysZone, length+1)
   let oldDV = dv; dv = newDV+1
   oldDV>>DV.type = dvTypeFree
   dv>>DV.type = dvTypeFile
   dv>>DV.length = length
   MoveBlock(lv dv>>DV.fp, lv oldDV>>DV.fp, lFP)
   CopyString(lv dv>>DV.name, name)
   Free(sysZone, name)
   name = lv dv>>DV.name
   logFlag = true
   ]

let fd = FindFile(dv>>DV.fp.leaderVirtualDa)
if fd eq 0 % fd>>FD.state eq fdBad then  //dv does not describe a wff.
   [
   logFlag = true
   fd = Salvage(dv, fd)  //but all may not be lost...
   test fd eq 0
      ifnot PutTemplate(dsp, "*N$PSalvaged $S>$S, SN $EUOb.",
        WouldHave, nil, dirName, name, lv dv>>DV.fp.serialNumber)
      ifso
         [
         PutTemplate(dsp, "*N$PDeleted entry for $S>$S, SN $EUOb.",
          WouldHave, nil, dirName, name, lv dv>>DV.fp.serialNumber)
         test newDV ne 0
            ifso Free(sysZone, newDV)
            ifnot dv>>DV.type = dvTypeFree
         return
         ] 
   ]

// FixSNs may have changed the file's SN
dv>>DV.fp.serialNumber.word1 = fd>>FD.sn.word1
dv>>DV.fp.serialNumber.word2 = fd>>FD.sn.word2
dv>>DV.fp.version = 1

test fd>>FD.registered 
   ifso PutTemplate(dsp, "*N$S>$S, SN $EUOb, is also listed under another name.",
    dirName, name, lv fd>>FD.sn)
   ifnot AdjustLeader(dv, fd)

if newDV ne 0 test dv>>DV.type eq dvTypeFile
   ifso Enqueue(newDvQ, newDV)
   ifnot Free(sysZone, newDV)
]

//-----------------------------------------------------------------------------------------
and Salvage(dv, fd) = valof 
//-----------------------------------------------------------------------------------------
// dv is an entry from a directory for which no valid FD exists.
// It may be that a chain of pages starts there, but that it is illegal
//  in some way, in which case all of the chain's pages are in badBT.
// If there is a chain, follow its forward links and repair minor damage,
//  truncating if major damage is encountered.
// Returns an fd if the file is salvaged, otherwise returns 0.
[
let prevVDA, curVDA, nextVDA = 0, dv>>DV.fp.leaderVirtualDa, nil
banzaiFlag = -1
if CheckedVDA(curVDA) eq -3 resultis 0
unless GetBit(badBT, curVDA) resultis 0
let pn = 0
until curVDA eq eofDA do
   [
   XferPage(DCreadLD, curVDA)
   RealDiskDA(sysDisk, prevVDA, lv label>>DL.previous)
   label>>DL.pageNumber = pn
   label>>DL.fileId.version = dv>>DV.fp.version
   label>>DL.fileId.serialNumber.word1 = dv>>DV.fp.serialNumber.word1
   label>>DL.fileId.serialNumber.word2 = dv>>DV.fp.serialNumber.word2 
   nextVDA = CheckedVDA(lv label>>DL.next)
   if nextVDA ugr maxVDA then nextVDA = eofDA
   test nextVDA eq eofDA
      ifso
         [
         if pn eq 0 resultis 0
         if label>>DL.numChars uge 512 then
            [
            PutTemplate(dsp, "*N$PSet numChars←511 bytes in last page of $S>$S, SN $EUOb.",
             WouldHave, nil, dirName, lv dv>>DV.name, lv dv>>DV.fp.serialNumber)
            label>>DL.next = 0
            label>>DL.numChars = 511
            ]
         ]
      ifnot test GetBit(badBT, nextVDA)
         ifso label>>DL.numChars = 512
         ifnot
            [
            if pn eq 0 resultis 0
            PutTemplate(dsp, "*N$PTruncated $S>$S, SN $EUOb, after page $D.",
             WouldHave, nil, dirName, lv dv>>DV.name, lv dv>>DV.fp.serialNumber, pn)
            label>>DL.next = 0
            label>>DL.numChars = 511
            nextVDA = eofDA  //to break the loop
            ]
   SetBit(badBT, curVDA, false)  //rescue page
   if alterFlag then XferPage(DCwriteLD, curVDA)
   pn = pn +1
   prevVDA = curVDA
   curVDA = nextVDA
   ]

// make an FD for the salvaged file
if fd eq 0 then fd = CreateD(fdbQ)
fd>>FD.firstVDA = dv>>DV.fp.leaderVirtualDa
fd>>FD.sn.word1 = dv>>DV.fp.serialNumber.word1
fd>>FD.sn.word2 = dv>>DV.fp.serialNumber.word2
fd>>FD.lastVDA = prevVDA
fd>>FD.lastPN = pn-1
fd>>FD.lastNumChars = label>>DL.numChars
resultis fd
]

//-----------------------------------------------------------------------------------------
and InsertFile(fd, nil) be
//-----------------------------------------------------------------------------------------
[
if fd>>FD.registered % fd>>FD.state ne fdGood return
logFlag = true

// find descriptor for this file's directory
XferPage(DCreadLD, fd>>FD.firstVDA)
let fp, dFD = lv data>>LD.dirFp, 0
if fp>>FP.serialNumber.directory then dFD = FindFile(fp>>FP.leaderVirtualDa)
if dFD eq 0 % dFD>>FD.state ne fdGood %
 dFD>>FD.sn.word1 ne fp>>FP.serialNumber.word1 %
 dFD>>FD.sn.word2 ne fp>>FP.serialNumber.word2 then dFD = FindFile(1)
OpenDir(dFD)

// check name syntax
let name = lv data>>LD.name
if alterFlag then unless LegalName(name) do
   name = GetName("*NFilename *"$S*" has bad syntax.", name)

// enter it in directory
let hd = vec lHD
if alterFlag then until FindFdEntry(dirSt, name, 0, 0, hd) eq -1 do
   [
   let newName = GetName("*NFilename *"$S*" is already in use.", name)
   if name ne lv data>>LD.name then Free(sysZone, name)
   name = newName
   ]
let dv = vec maxLenDV; Zero(lv dv>>DV.fp, lFP)
dv>>DV.fp.serialNumber.word1 = fd>>FD.sn.word1
dv>>DV.fp.serialNumber.word2 = fd>>FD.sn.word2
dv>>DV.fp.version = 1
dv>>DV.fp.leaderVirtualDa = fd>>FD.firstVDA
if alterFlag then MakeNewFdEntry(dirSt, name, dv, hd, 0)
PutTemplate(dsp, "*N$PInserted $S, SN $EUOb, in directory $S.",
 WouldHave, nil, name, lv dv>>DV.fp.serialNumber, dirName)

CopyString(lv dv>>DV.name, name)
if name ne lv data>>LD.name then Free(sysZone, name)
AdjustLeader(dv, fd)
]

//-----------------------------------------------------------------------------------------
and AdjustLeader(dv, fd) be
//-----------------------------------------------------------------------------------------
// Make leader page of fd match dv
[
XferPage(DCreadLD, fd>>FD.firstVDA)
  
// set symbolic name
CopyString(lv data>>LD.name, lv dv>>DV.name)
  
// set directory back pointer
let fp = lv data>>LD.dirFp
fp>>FP.serialNumber.word1 = dirFD>>FD.sn.word1
fp>>FP.serialNumber.word2 = dirFD>>FD.sn.word2
fp>>FP.version = 1
fp>>FP.leaderVirtualDa = dirFD>>FD.firstVDA

// set last page hint
let fa = lv data>>LD.hintLastPageFa
fa>>FA.da = fd>>FD.lastVDA
fa>>FA.pageNumber = fd>>FD.lastPN
fa>>FA.charPos = fd>>FD.lastNumChars

// put disk shape in leader page of SysDir
if dv>>DV.fp.leaderVirtualDa eq 1 then
   [
   let ds = data + FindDShape(data)
   ds>>FPROP.type = fpropTypeDShape
   ds>>FPROP.length = lDSHAPE +1
   ds = ds +1
   ds>>DSHAPE.nDisks = sysDisk>>BFSDSK.nDisks
   ds>>DSHAPE.nTracks = sysDisk>>BFSDSK.nTracks
   ds>>DSHAPE.nHeads = sysDisk>>BFSDSK.nHeads
   ds>>DSHAPE.nSectors = sysDisk>>BFSDSK.nSectors
   ]

if alterFlag then XferPage(DCwriteD, fd>>FD.firstVDA)
fd>>FD.registered = true
]

//-----------------------------------------------------------------------------------------
and OpenDir(fd) be
//-----------------------------------------------------------------------------------------
[
if fd eq dirFD return  //already open
CloseDir()
dirFD = fd
let dirFP = vec lFP; Zero(dirFP, lFP)
MoveBlock(lv dirFP>>FP.serialNumber, lv fd>>FD.sn, lSN)
dirFP>>FP.version = 1
dirFP>>FP.leaderVirtualDa = fd>>FD.firstVDA
dirSt = CreateDiskStream(dirFP, (alterFlag? ksTypeReadWrite, ksTypeReadOnly))
ReadLeaderPage(dirSt, data)
let dn = LegalName(lv data>>LD.name)? lv data>>LD.name, "?."
dirName = Allocate(sysZone, dn>>String.length rshift 1 +1)
CopyString(dirName, dn)
dirName>>String.length = dirName>>String.length -1  //remove trailing "."
]

//-----------------------------------------------------------------------------------------
and CloseDir() be
//-----------------------------------------------------------------------------------------
[
dirFD = 0
if dirSt then Closes(dirSt); dirSt = 0
if dirName then Free(sysZone, dirName); dirName = 0
]

//-----------------------------------------------------------------------------------------
and GetName(string, arg) = valof
//-----------------------------------------------------------------------------------------
[
PutTemplate(dsp, string, arg)
   [
   let name = GetString("*NType a new name: ")
   if name>>String.char↑(name>>String.length) ne $. then
      [
      name>>String.char↑(name>>String.length+1) = $.
      name>>String.length = name>>String.length +1
      Puts(dsp, $.)
      ]
   if LegalName(name) resultis name
   Ws(" - bad syntax")
   Free(sysZone, name)
   ] repeat
]

//-----------------------------------------------------------------------------------------
and LegalName(string) = valof
//-----------------------------------------------------------------------------------------
[
let length = string>>String.length
if length eq 0 % length gr maxLengthFn resultis false
for i = 1 to length-1 do
   [
   let c = string>>String.char↑i
   unless ($a le c & c le $z) % ($A le c & c le $Z) % ($0 le c & c le $9) %
    c eq $. % c eq $+ % c eq $- % c eq $! % c eq $$ resultis false
   ]
resultis string>>String.char↑length eq $.
]

//-----------------------------------------------------------------------------------------
and FindFile(firstVDA) = valof
//-----------------------------------------------------------------------------------------
[
let v, f = firstVDA, 0
ForAllFDs(MatchFD, lv v)
resultis f
]

//-----------------------------------------------------------------------------------------
and MatchFD(fd, lvV) be if lvV!0 eq fd>>FD.firstVDA then lvV!1 = fd
//-----------------------------------------------------------------------------------------