// OldDirs.Bcpl -- directory stuff -- includes version number code
// Copyright Xerox Corporation 1979
// Last modified September 18, 1979  2:27 PM by Taft

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

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

// incoming procedures
// from stream stuff
Gets; Puts; Resets; Endofs; Errors; Closes
ReadBlock; WriteBlock; SetFilePos; GetCompleteFa
CreateDiskStream; KsGetDisk

//Alloc
Allocate; Free

// from bfs
CreateDiskFile; DeleteDiskPages; ActOnDiskPages

// from OS
SysErr

// miscellaneous
MoveBlock; Zero; SetBlock; Noop
Dvec; DefaultArgs; ReadCalendar

// outgoing static
dirVersions

// incoming statics
sysZone; sysDisk
]

static [ dirVersions = true ]

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

// version control masks (see definitions of verxxx in streams.d)
verNewMask = #140000	//For testing eq verNew
verNewAMask = #170000	//For testing eq verNewAlways
verOlMask = #100000	//if non-zero, verNew % verOldest
verCrMask = #40000	//if non-zero, create file ok
verKeepMask = #77	//Only look at this many bits of version #
]

// hd: hole descriptor
//---------------------------------------------------------------------------
structure HD:[ maxSize word; neededSize word; pos word ]
//---------------------------------------------------------------------------

// FindFdEntry returns the position, or -1 if the name is not found.
// The name is not parsed (except to find the version number),
// so this should have already been done (see ParseFileName).
// 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.
//
// Version stuff works as follows:
//	If there is a version in the "name," then it takes precedence
//	   over any other strategy.
//	Otherwise, look at versionControl:
//	= verLatest(default) => take highest version number
//	= verLatestCreate => ditto
//	= verOldest => take lowest version number
//	= verNew+nToKeep => we are making a new version, (1+highest
//	      version found during scan of directory) = newV.
//	    if nToKeep eq 0 then nToKeep = defaultVersionsKept
//	    if we find a file with version ls newV-nToKeep, it can
//	      be re-used bodily; return it in dv, with pos ne -1.
// Always appends to the "name" the version needed.

//---------------------------------------------------------------------------
let FindFdEntry(dirS, name, compareFn, dv, hd, versionControl, extraSpace,
   defaultVersionsKept; numargs na) = valof
//---------------------------------------------------------------------------
[
// ignores difference between upper and lower case
manifest fnMask = not (#40*(#400+1)) //137,,137 in case you didn't guess...
compileif lHD ne size HD/16 then [ foo = nil ]

let v = vec lDV; let h = vec lHD
let disk = KsGetDisk(dirS)
DefaultArgs(lv na, -2, 0, v, h, verLatest, 0,
 disk>>DSK.diskKd>>KDH.defaultVersionsKept)

Zero(hd, lHD); hd>>HD.neededSize = extraSpace
let lNameInWords = 0
let givenVersion = 0
let givenVersionFlag = false;
if compareFn eq 0 then
   [
   givenVersion = StripVersion(name, lv givenVersionFlag)
   // 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
   // the 2 here is for the version number (!12) -- takes 2 words max
   hd>>HD.neededSize = t rshift 1+2+lDV+extraSpace
   if hd>>HD.neededSize gr maxDeSize then
      hd>>HD.neededSize = maxDeSize
   ]
let thisDv = vec lDV
// "best" means largest version # if versionControl = verLatest else oldest
let verNewFlag = (versionControl & verNewMask) eq verNew
let bestVersion = -1
let bestPos = -1
let highestVersion = 0
let thisName = vec maxDeSize-lDV
Resets(dirS); let pos = 0

until Endofs(dirS) do
   [
   for i = 0 to lDV-1 do thisDv!i = Gets(dirS)
   let l = 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 hd>>HD.maxSize ls hd>>HD.neededSize then
            hd>>HD.maxSize = hd>>HD.maxSize+l
         SetFilePos(dirS, 0, 2*(pos+l))
         endcase
         ]
      case dvTypeFile:
         [
         if hd>>HD.maxSize ls hd>>HD.neededSize then
            [ hd>>HD.maxSize = 0; hd>>HD.pos = pos+l ]
         // keep a malformed directory entry from overflowing thisName
         if l gr maxDeSize then SysErr(dirS, ecDeTooBig, pos)
         for i = 0 to l-(lDV+1) do thisName!i = Gets(dirS)
         test compareFn eq 0
            ifso
               [  //default system compareFn
               let thisVersion = StripVersion(thisName)
               // the name, without version number, must match
               for i = 0 to lNameInWords-1 do
                  if ((thisName!i xor name!i) & fnMask) ne 0 endcase
               // if it does and we want a specific version, then check for
               // it also, if version stuff is disabled completely, get out
               // early....
               if givenVersionFlag ne 0 % defaultVersionsKept le 0 then 
                  test givenVersion eq thisVersion
                     ifso
                        [
                        MoveBlock(dv, thisDv, lDV)
                        bestPos = pos
                        break
                        ]
                     ifnot endcase
               // we want the highest (or lowest) version
               if thisVersion gr highestVersion then
                  highestVersion = thisVersion
               if (bestVersion eq -1) % ((versionControl&verOlMask) eq 0 ?
                thisVersion gr bestVersion, thisVersion ls bestVersion) then
                  [
                  MoveBlock(dv, thisDv, lDV)
                  bestPos = pos
                  bestVersion = thisVersion
                  ]
               // passing over a file while creating a new one means we need
               // to mark the "name" -> FP map invalid:
               if verNewFlag then BumpLeaderVersion(disk, lv thisDv>>DV.fp)
               endcase
               ]
            ifnot if compareFn(name, thisName, thisDv) eq 0 resultis pos
         endcase
         ]
      default:  // unknown type of directory entry
         [
         Errors(dirS, ecBadDeType, pos)
         SetFilePos(dirS, 0, 2*(pos +l))  //skip it.
         endcase
         ]
      ]
   pos = pos +l
   ]

// if we didn't get any match, bestPos will still be -1, indicating failure
if compareFn eq 0 then
   [
   test givenVersionFlag ne 0
      ifso bestVersion = givenVersion
      ifnot if verNewFlag then
         [
         let keep = defaultVersionsKept
         let okeep = versionControl & verKeepMask
         if okeep then keep = okeep
         if defaultVersionsKept le 0 then keep = 1

         // make a "new" file (vn = vn+1) only if there is a previous version
         // and it had a vn or if verNewAlways:
         if defaultVersionsKept gr 0 & (highestVersion gr 0 %
          (versionControl & verNewAMask) eq verNewAlways) then
            [
            // if found file, but can't delete, return pos = -1
            // (never happens if versions are disabled):
            if highestVersion - keep +1 ls bestVersion then bestPos = -1
            bestVersion = highestVersion +1
            ]
         ]

   // append the version -- by convention, no version means version 1
   if bestVersion gr 0 then AppendVersion(name, bestVersion)

   // now revise the space estimate to be exact
   hd>>HD.neededSize = (name>>STRING.length +2) rshift 1 + lDV + extraSpace
   ]

resultis bestPos
]

//---------------------------------------------------------------------------
and BumpLeaderVersion(disk, fp) be
//---------------------------------------------------------------------------
[
let ld = 1 lshift (disk>>DSK.lnPageSize)
Dvec(BumpLeaderVersion, lv ld)
let DAs = vec 3; SetBlock(DAs, fillInDA, 3)
DAs!1 = fp>>FP.leaderVirtualDa
ActOnDiskPages(disk, lv ld, DAs+1, fp, 0, 0, DCreadD)
ld>>LD.changeSerial = ld>>LD.changeSerial +1
ActOnDiskPages(disk, lv ld, DAs+1, fp, 0, 0, DCwriteD)
]

//---------------------------------------------------------------------------
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 nadFreeDeSize; 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+2) rshift 1
if lNameInWords ge 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
   [
   let h = 0; 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, and a directory stream
// in which to look for the name.
// list!0 = errRtn; list!1 = zone; list!3 = disk

let L = n>>STRING.length; let sep = 0
for i = 1 to L 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, L)

// 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
let errRtn = list!0
if dirFn!0 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 ]
   ]
let s = OpenFile(dirFn, 0, 0, versionControl, fp, errRtn, list!1,
 0, disk, 0, #100000)
// if s eq 0 then errRtn(dirFn, ecBadDirName)
resultis s
]

//---------------------------------------------------------------------------
and ExtractLegalFileName(srcS, destS, firstMinus1, last) be
//---------------------------------------------------------------------------
[
let L = last-firstMinus1
if L ls 0 then L = 0
Zero(destS, (L+3) rshift 1)
if L eq 0 then return
let char = nil
for i = 1 to L do  //make sure each character is legal
   [
   char = srcS>>STRING.char↑(firstMinus1+i)
   unless (char ge $A & char le $Z) % (char ge $a & char le $z) %
    (char ge $0 & char le $9) % (char eq $-) % (char eq $$) %
    (char eq $!) % (char eq $?) % (char eq $+) % (char eq $.) %
    (char eq $<) % (char eq $>) do
      char = $-  // <--------===
   destS>>STRING.char↑i = char
   ]
if char ne $. then [ L = L+1 ; destS>>STRING.char↑L = $. ]
destS>>STRING.length = L
]

//---------------------------------------------------------------------------
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 StripVersion(fn, lvVersionExists; numargs na) = valof
//---------------------------------------------------------------------------
// adjusts the length of fn to remove the version number and attaches a
// final $., and returns the version, or 0 if there wasn't one.
[
if na eq 1 then lvVersionExists = lv na
@lvVersionExists = false
let lFn = fn>>STRING.length
let version = 0
let multiplier = 1
for i = 0 to lFn-1 do
   [
   let c = fn>>STRING.char↑(lFn-i)
   switchon c into
      [
      case $0 to $9:
         [
         version = version+(c-$0)*multiplier
         multiplier = multiplier*10
         loop
         ]
      case $!:
         [
         lFn = lFn-i-1
         @lvVersionExists = true
         break
         ]
      case $.: loop
      default: [ version = 0; break ]
      ]
   ]
if fn>>STRING.char↑lFn ne $. then
   [
   lFn = lFn+1
   fn>>STRING.char↑lFn = $.
   ]
fn>>STRING.length = lFn
resultis version
]

//---------------------------------------------------------------------------
and AppendVersion(fileName, version) be
//---------------------------------------------------------------------------
[
let l = fileName>>STRING.length
if fileName>>STRING.char↑l eq $. then fileName>>STRING.length = l-1
if version gr 0 then AppendChar(fileName, $!)
if version ge 100 then AppendChar(fileName, (version/100) rem 10 + $0)
if version ge 10 then AppendChar(fileName, (version/10) rem 10 + $0)
if version ge 1 then AppendChar(fileName, version rem 10 + $0)
AppendChar(fileName, $.)
]

//---------------------------------------------------------------------------
and AppendChar(string, char) be
//---------------------------------------------------------------------------
[
let l = string>>STRING.length+1
string>>STRING.char↑l = char
string>>STRING.length = l
]

//---------------------------------------------------------------------------
and OpenFile(name, ksType, itemSize, versionControl, hintFp, errRtn,
 zone, nil, disk, CreateStream, SNword; numargs na) = valof
//---------------------------------------------------------------------------
//Note:  if a file is re-used without changing its fp, its creation date
// is still changed.  This isn't quite right, but then not much about
// this module is.
[
DefaultArgs(lv na, -1, ksTypeReadWrite, wordItem, 0, 0, SysErr,
 sysZone, 0, sysDisk, CreateDiskStream, 0)
if versionControl eq 0 then
   versionControl = (ksType eq ksTypeReadOnly)? verLatest,
    ((ksType eq ksTypeWriteOnly)? verNew, verLatestCreate)
let s = 0

// following check attempts to decide whether hint is really filled in
// with anything.  It may be that user just wants it filled in.
if hintFp ne 0 & hintFp>>FP.version ne 0 then
   s = CreateStream(hintFp, ksType, itemSize, Noop, errRtn, zone,
    nil, disk)

if s eq 0 then  //hint failed, or wasn't supplied
   [
   //blunder check
   if name eq 0 % name>>STRING.length eq 0 then 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 false  //bad directory name
   // go look in the directory for the file. Return with the name having
   // the appropriate version number slapped on the end.
   let pos = FindFdEntry(currentDirS, fixedName, 0, dv, hd, versionControl)

   let makeit = false
   test pos eq -1
      ifso
         [
         // Either virgin file name or unable to delete older
         // version of same name
         if (versionControl & verCrMask) eq 0 then
            [ Closes(currentDirS); resultis 0 ]
         makeit = true  // make afresh
         ]
      ifnot if (versionControl & verNewMask) eq verNew &
       disk>>DSK.diskKd>>KDH.defaultVersionsKept gr 0 then
         [
         // Found an old version that could be re-used. It is in dv.
         // Note that if there is no versioning going on,
         // we should not re-make the file.
         DeleteFdEntry(currentDirS, pos)
         makeit = 1  // but re-use file alloc'ed
         ]

   if makeit ne 0 then
      [
      let dirCfa = vec lCFA; GetCompleteFa(currentDirS, dirCfa)
      CreateDiskFile(disk, fixedName, lv dv>>DV.fp, lv dirCfa>>CFA.fp,
       SNword, makeit eq 1)
      MakeNewFdEntry(currentDirS, fixedName, dv, hd)
      ]

   Closes(currentDirS)
   if hintFp then MoveBlock(hintFp, lv dv>>DV.fp, lFP)
   s = CreateStream(lv dv>>DV.fp, ksType, itemSize, Noop, errRtn,
    zone, nil, disk)
   if s eq 0 then errRtn(s, ecCantOpenFile)
   ]

resultis s
]

//---------------------------------------------------------------------------
and OpenFileFromFp(fp) = OpenFile(0, 0, 0, 0, fp)
//---------------------------------------------------------------------------

//---------------------------------------------------------------------------
and DeleteFile(name, versionControl, errRtn, zone, nil,
 disk; numargs n) = valof
//---------------------------------------------------------------------------
// returns false if it couldn't find the file
[
DefaultArgs(lv n, -1, verOldest, SysErr, sysZone, 0, 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, 0, versionControl)
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
]