// ScavScan.bcpl -- analyzes the disk
// Copyright Xerox Corporation 1979, 1980, 1981, 1982
// Last modified April 29, 1982  10:32 AM by Boggs

// This is one of two implementations of the Scan pass of the Scavenger.
// This version uses a bounded amount of storage at the cost of possibly
//  running very slowly.  FChain runs in time proportional to the square
//  of the number of pages in a file.  This is fine for files up to a few
//  hundred pages, but Lisp.VirtualMem (6,000 pages) can take 1/2 hour!

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

external
[
// outgoing procedures
ScanDisk; CreateD; MapFiles

// incoming procedures
XferPage; XferError; GetBit; SetBit; Ws
GetString; Confirm; FChain; DoubleUsc

VirtualDiskDA; InitializeDiskCBZ
GetDiskCb; DoDiskCommand
Idle; Noop; Zero; SetBlock; MoveBlock; TruePredicate
Allocate; Free; Enqueue; Dequeue
PutTemplate

// outgoing statics
usedBT; badBT; fdbQ

// incoming statics
sysZone; sysDisk; dsp; log
maxVDA; alterFlag; data
logFlag; pt
]

static
[
usedBT; badBT
backBT; banzaiFlag; nFD; fdbQ
]

manifest
[
free = 77777b
bad = 77776b
good = 77775b
incor = 77774b
]

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

// pt!i may be good, bad, free, incor, or
// a nextP from the disk <40000b, or
// 100000b % i where i is the index of a file descriptor.
structure NP:		// Next pointer array element
[
lastPage bit 1
fdi bit 15		// fd index
]

//-----------------------------------------------------------------------------------------
let ScanDisk() be
//-----------------------------------------------------------------------------------------
[
Zero(pt, maxVDA+1)
backBT = Allocate(sysZone, maxVDA/16+1); Zero(backBT, maxVDA/16+1)
// N.B. pt is a word array but backBT is a bit array

// Phase 1: Sweep the disk and record all the nextPs
//          in pt and mark confirmed backPs in backBT
SweepDisk(ScanCleanup, TruePredicate, lv ScanError)

// Phase 2: check all the linkages 
SweepDisk(CheckBackPs, UnConfirmed)
// bit i of backBT is set if page i is a leader page (backP=0) or if
//  the page pointed to by i's backP points at page i (via its nextP)
for i = 1 to maxVDA if 1 ule pt!i & pt!i ule maxVDA test pt!(pt!i) eq free
   ifso pt!(pt!i) = bad
   ifnot if GetBit(backBT, pt!i) eq 0 then pt!i = bad
Free(sysZone, backBT)
// At this point all wfc's are represented by chains through pt
// which have no converging pointers since each link is confirmed
// by a back-pointer.  The cell pointed to by the last cell
// of a wfc is either bad or 0.  Circular wfc's are possible.
// pt!i = 0 => i is a plausible last page
//      = free => i is free (definitely)
//      = bad => i is bad
//      = incor => i is effectively bad during phase 3
//      = x => i>>NEXTP is a legal disk address and is
//		confirmed by the backP. i is part of a wfc.

// Phase 3: Discover all the wff's by sweeping the disk again.
//  We need only consider pages which are part of wfc's or
//  have pt!i = 0; others cannot be part of a wff.
// Invariant: let l be the page pointed to by the last page of a
//  wfc. If pt!l is bad then the wfc is not part of a wff. Otherwise
//  pt!l = 0 or pt!l<<NP.lastPage = 1. In the latter case pt!l<<NP.fdi
//  is a pointer to a file descriptor. All the pages on the wfc
//  (plus page l) so far encountered are consistent with this file
//  descriptor. If FD.firstVDA is 0 the first page of the file has not
//  been seen yet. Each active file descriptor is pointed to by just
//  one cell and represents a tentative wff.
fdbQ = Allocate(sysZone, 2); fdbQ!0 = 0
SweepDisk(CheckPage, Alive)

// Phase 4: Construct bit tables
MapFiles(MarkGood)

// Create used-page and bad-page bit-tables
usedBT = Allocate(sysZone, maxVDA/16+1); Zero(usedBT, maxVDA/16+1)
badBT = Allocate(sysZone, maxVDA/16+1); Zero(badBT, maxVDA/16+1)
for vda = 1 to maxVDA do
   [
   let p = pt!vda
   test p eq free
      ifso sysDisk>>BFSDSK.freePages = sysDisk>>BFSDSK.freePages +1
      ifnot SetBit(usedBT, vda, true)
   unless p eq good % p eq free % p eq incor do
      [ SetBit(badBT, vda, true); logFlag = true ]
   ]
SetBit(usedBT, 0, true)  //page zero, Sys.Boot's boot loader, is special
for vda = maxVDA+1 to (maxVDA/16)*16+15 do SetBit(usedBT, vda, true)
]

//-----------------------------------------------------------------------------------------
and ScanCleanup(disk, cb, cbz) be
//-----------------------------------------------------------------------------------------
[
let vda = VirtualDiskDA(sysDisk, lv cb>>CB.diskAddress)
let dl = cb>>CB.labelAddress

// Check for special labels
let fid = lv dl>>DL.fileId
if fid!0 eq -1 & fid!1 eq -1 & fid!2 eq -1 then [ pt!vda = free; return ]
if fid!0 eq -2 & fid!1 eq -2 & fid!2 eq -2 then [ pt!vda = incor; return ]

let pn = dl>>DL.pageNumber
let nc = dl>>DL.numChars
let nextP = CheckedVDA(lv dl>>DL.next)
let backP = CheckedVDA(lv dl>>DL.previous)
pt!vda = nextP eq eofDA? 0, nextP

// Check for obvious damage
if nc ugr 512 % pn ugr maxVDA % nextP eq vda % backP eq vda %
 nextP eq eofDA & (nc eq 512 % pn eq 0 % backP eq eofDA) %
 backP eq eofDA & (nc ne 512 % pn ne 0) %
 nextP ne eofDA & (nc ne 512 % nextP ugr maxVDA) %
 backP ne eofDA & (pn eq 0 % backP ugr maxVDA) then [ pt!vda = bad; return ]

test backP eq eofDA
  ifso SetBit(backBT, vda, true)
  ifnot if backP uls vda test pt!backP eq vda
     ifso SetBit(backBT, vda, true)
     ifnot pt!vda = bad
]

//-----------------------------------------------------------------------------------------
and ScanError(nil, cb, nil) be
//-----------------------------------------------------------------------------------------
[
XferError(nil, cb, nil)
let vda = VirtualDiskDA(sysDisk, lv cb>>CB.diskAddress)
let header, label, data = vec 2, cb>>CB.labelAddress, cb>>CB.dataAddress
if cb>>CB.status<<DST.finalStatus eq checkError then
   [  //header check error
   XferPage(DCreadHLD, vda, data, label, header, lv Noop)
   if (cb>>CB.diskAddress xor 2) eq header>>DH.diskAddress & 
    not banzaiFlag then
      [
      PutTemplate(dsp, "*NThe disk in drive $D looks like disk $D ",
       cb>>CB.diskAddress.disk, header>>DH.diskAddress.disk)
      banzaiFlag = Banzai("of a file system.")
      ]
   ]
if alterFlag then if Confirm("*NMay I rewrite the page?") then
   [
   XferPage(DCwriteHLD, vda, data, label, 0, lv Noop)
   if (XferPage(DCreadD, vda, data, label, 0, lv Noop) &
    DSTgoodStatusMask) ne DSTgoodStatus then
      [
      Ws(".  It's incorrigable.")
      SetBlock(lv label>>DL.fileId, -2, lFID)
      XferPage(DCwriteHLD, vda, data, label, 0, lv Noop)
      ]
   ]
]

//-----------------------------------------------------------------------------------------
and CheckedVDA(lvRealDA) = valof
//-----------------------------------------------------------------------------------------
[
if lvRealDA>>DA.restore ne 0 resultis -3
if lvRealDA>>DA.sector ge sysDisk>>BFSDSK.nSectors resultis -3
if lvRealDA>>DA.track ge sysDisk>>BFSDSK.nTracks resultis -3
if lvRealDA>>DA.disk ge sysDisk>>BFSDSK.nDisks then
   [
   unless banzaiFlag do
      [
      Ws("*NThis looks like part of a two disk file system, ")
      banzaiFlag = Banzai("but you are scavenging a single disk.")
      ]
   resultis -3
   ]
resultis VirtualDiskDA(sysDisk, lvRealDA)
]

//-----------------------------------------------------------------------------------------
and Banzai(string) = valof
//-----------------------------------------------------------------------------------------
[
Ws(string)
Ws("*NCheck your disks, then type *"BANZAI!*" ")
let answer = GetString("if you wish to continue scavenging: ")
let banzai = "BANZAI!"
if answer>>String.length ne banzai>>String.length finish
for i = 1 to banzai>>String.length do
   [
   let c1 = answer>>String.char↑i
   if c1 ge $a & c1 le $z then c1 = c1-($a-$A)
   let c2 = banzai>>String.char↑i
   if c2 ge $a & c2 le $z then c2 = c2-($a-$A)
   if c1 ne c2 finish
   ]
Free(sysZone, answer)
resultis true
]

//-----------------------------------------------------------------------------------------
and CheckBackPs(disk, cb, cbz) be
//-----------------------------------------------------------------------------------------
[
let vda = VirtualDiskDA(sysDisk, lv cb>>CB.diskAddress)
test pt!VirtualDiskDA(sysDisk, lv cb>>CB.label.previous) eq vda
   ifso SetBit(backBT, vda, true)
   ifnot pt!vda = bad
]

//-----------------------------------------------------------------------------------------
and UnConfirmed(vda) = 0 ule pt!vda & pt!vda ule maxVDA & GetBit(backBT, vda) eq 0
//-----------------------------------------------------------------------------------------

//-----------------------------------------------------------------------------------------
and CheckPage(disk, cb, cbz) be
//-----------------------------------------------------------------------------------------
[
let vda = VirtualDiskDA(sysDisk, lv cb>>CB.diskAddress)
let sn = lv cb>>CB.label.fileId.serialNumber
let pn = cb>>CB.label.pageNumber
let lvLast = FChain(pt, vda, lv pn)

if lvLast ne 0 then  //fd found
   [
   let fd = nil
   test @lvLast eq 0 //first encounter with file?
      ifso
         [
         fd = CreateD()
         @lvLast = nFD
         MoveBlock(lv fd>>FD.sn, sn, 2)
         fd>>FD.lastPN = pn
         lvLast>>NP.lastPage = 1
         ]
      ifnot //check
         [
         let fdb, fdi, i = fdbQ!0, lvLast>>NP.fdi, 1
         while fdb ne 0 test i le fdi & fdi ls i+fdb>>DB.ptr
            ifnot [ i = i+fdb>>DB.ptr; fdb = fdb>>DB.link ]
            ifso [ fd = fdb+lenDB+(fdi-i)*lenFD; break ]
         unless fd>>FD.lastPN eq pn & DoubleUsc(lv fd>>FD.sn, sn) eq 0 do
            [ fd>>FD.firstVDA = 0; @lvLast = bad ]
         ]
   if cb>>CB.label.previous eq 0 then fd>>FD.firstVDA = vda
   if cb>>CB.label.next eq 0 then
      [
      fd>>FD.lastVDA = vda
      fd>>FD.lastNumChars = cb>>CB.label.numChars
      ]
   ]
]

//-----------------------------------------------------------------------------------------
and Alive(vda) = 0 ule pt!vda & pt!vda ule maxVDA
//-----------------------------------------------------------------------------------------

//-----------------------------------------------------------------------------------------
and MarkGood(fd) be
//-----------------------------------------------------------------------------------------
[
let vda = fd>>FD.firstVDA
test fd>>FD.lastPN eq 0 // wff must have 2 pages
   ifso fd>>FD.firstVDA = 0
   ifnot [ let t = pt!vda; pt!vda = good; vda = t ] repeatuntil vda ls 0
]

//-----------------------------------------------------------------------------------------
and CreateD() = valof
//-----------------------------------------------------------------------------------------
// Returns an FD.
[
manifest dbSize = 240 + lenDB  //divisible by lenFD and lenRD
let fdb = fdbQ!1
if fdbQ!0 eq 0 % fdb>>DB.ptr eq (dbSize-lenDB)/lenFD then
   [
   fdb = Allocate(sysZone, dbSize); Zero(fdb, dbSize)
   Enqueue(fdbQ, fdb)
   ]
let fd = fdb + lenDB + fdb>>DB.ptr*lenFD
fdb>>DB.ptr = fdb>>DB.ptr +1
nFD = nFD +1
resultis fd
]

//-----------------------------------------------------------------------------------------
and MapFiles(Proc) be
//-----------------------------------------------------------------------------------------
// Enumerates all active FDs and calls Proc(fd)
[
let fdb = fdbQ!0
while fdb ne 0 do
   [
   for i = 0 to fdb>>DB.ptr-1 do
      [
      let fd = fdb + lenDB + i*lenFD
      if fd>>FD.firstVDA then Proc(fd)
      ]
   fdb = fdb>>DB.link
   ]
]

//-----------------------------------------------------------------------------------------
and SweepDisk(Proc, Live, lvError; numargs na) be
//-----------------------------------------------------------------------------------------
[
let zoneLength = sysDisk>>DSK.lengthCBZ +
 2*sysDisk>>BFSDSK.nSectors*sysDisk>>DSK.lengthCB
let cbz = Allocate(sysZone, zoneLength)
InitializeDiskCBZ(sysDisk, cbz, 0, zoneLength, SweepRetry,
 (na gr 2? lvError, lv XferError))
cbz>>CBZ.cleanupRoutine = Proc
cbz>>CBZ.errorDA = 1

SweepRetry: let sweepVDA = cbz>>CBZ.errorDA
while sweepVDA le maxVDA do
   [
   if Live(sweepVDA) then DoDiskCommand(sysDisk, GetDiskCb(sysDisk, cbz),
    data, sweepVDA, 0, 0, DCreadLD)
   sweepVDA = sweepVDA +1
   if (sweepVDA & 77b) eq 0 then Idle()
   ]

while @cbz>>CBZ.queueHead ne 0 do GetDiskCb(sysDisk, cbz)
Free(sysZone, cbz)
]