-- File: MailScavenger.mesa -- Last edited by Levin: 3-Sep-80 19:03:28 DIRECTORY AltoDefs USING [PageSize], AltoFileDefs USING [FilenameChars], CharIO USING [ NumberFormat, PutChar, PutNumber, PutString], DiskKDDefs USING [CountFreeDiskPages], ImageDefs USING [BcdTime], InlineDefs USING [LowHalf], IODefs USING [ CR, ESC, LineOverflow, NUL, ReadChar, ReadID, Rubout, SP, WriteChar, WriteDecimal, WriteLine, WriteString], SegmentDefs USING [ AccessOptions, Append, DestroyFile, FileHandle, FileNameError, GetEndOfFile, LockFile, NewFile, NewFileOnly, OldFileOnly, PageNumber, Read, ReleaseFile, SetFileAccess, UnlockFile, Write], StreamDefs USING [ AccessOptions, Append, CleanupDiskStream, CreateByteStream, DiskHandle, FileLength, GetPosition, IndexToPosition, Read, ReadBlock, SetPosition, StreamError, StreamPosition, Write, WriteBlock], StringDefs USING [AppendChar, AppendString, EqualString], SystemDefs USING [AllocatePages, FreePages], TimeDefs USING [AppendDayTime, UnpackDT]; MailScavenger: PROGRAM IMPORTS CharIO, DiskKDDefs, ImageDefs, InlineDefs, IODefs, SegmentDefs, StreamDefs, StringDefs, SystemDefs, TimeDefs = BEGIN mailFile: STRING ← [AltoFileDefs.FilenameChars]; scratchFile: STRING = "MailScavenger.scratch$"; mailF, scratchF: SegmentDefs.FileHandle; inS, outS: StreamDefs.DiskHandle; readFileAccess: SegmentDefs.AccessOptions = SegmentDefs.Read; writeFileAccess: SegmentDefs.AccessOptions = SegmentDefs.Write + SegmentDefs.Append; allFileAccess: SegmentDefs.AccessOptions = readFileAccess + writeFileAccess; readStreamAccess: StreamDefs.AccessOptions = StreamDefs.Read; writeStreamAccess: StreamDefs.AccessOptions = StreamDefs.Write + StreamDefs.Append; allStreamAccess: StreamDefs.AccessOptions = readStreamAccess + writeStreamAccess; pageSize: CARDINAL = AltoDefs.PageSize; changesMade: BOOLEAN; safetySlop: CARDINAL = 10; maxPlausibleChars: CARDINAL = 60000; absoluteLimit: CARDINAL = maxPlausibleChars + 1000; compactionMark: CHARACTER = 003C; Initialize: PROCEDURE RETURNS [goAhead: BOOLEAN] = BEGIN OPEN IODefs, SegmentDefs, StreamDefs; ENABLE Rubout => {goAhead ← FALSE; CONTINUE}; pages: CARDINAL; WriteHerald[]; DO WriteString["Mail file to scavenge: "L]; mailFile.length ← 0; ReadID[mailFile ! LineOverflow => CONTINUE]; FOR i: CARDINAL IN [0..mailFile.length) DO IF mailFile[i] = '. THEN EXIT; REPEAT FINISHED => BEGIN defaultExtension: STRING = ".mail"L; StringDefs.AppendString[mailFile, defaultExtension]; WriteString[defaultExtension]; END; ENDLOOP; mailF ← NewFile[mailFile, readFileAccess ! FileNameError => {WriteLine["...can't be opened!"L]; LOOP} ]; WriteChar[CR]; EXIT ENDLOOP; pages ← GetEndOfFile[mailF].page + 1; IF pages + safetySlop > DiskKDDefs.CountFreeDiskPages[] THEN BEGIN WriteString["Sorry, but I will need a minimum of "]; WriteDecimal[pages]; WriteString[" free disk pages before I can scavenge "]; WriteLine[mailFile]; ReleaseFile[mailF]; RETURN[FALSE] END; LockFile[mailF]; inS ← CreateByteStream[mailF, readStreamAccess]; BEGIN ENABLE FileNameError => CONTINUE; scratchF ← NewFile[scratchFile, allFileAccess, OldFileOnly]; DestroyFile[scratchF]; END; scratchF ← NewFile[scratchFile, allFileAccess, NewFileOnly]; LockFile[scratchF]; outS ← CreateByteStream[scratchF, allStreamAccess]; changesMade ← FALSE; RETURN[TRUE] END; WriteHerald: PROCEDURE = BEGIN OPEN IODefs, TimeDefs; time: STRING ← [20]; WriteString["Mail File Scavenger of "L]; AppendDayTime[time, UnpackDT[ImageDefs.BcdTime[]]]; WriteLine[time]; WriteChar[CR]; END; Scavenge: PROCEDURE = BEGIN OPEN CharIO, StreamDefs; inLength: StreamPosition = IndexToPosition[FileLength[inS]]; stampStart: STRING = "*start* "L; stampLength: CARDINAL = 8 + 2*(5+1) + 3 + 1; deletedFlag, seenFlag, markChar: CHARACTER; messageSize: CARDINAL; lastStampIsPrototype: BOOLEAN; ticksSinceLastMessage: BOOLEAN ← FALSE; messageNumber: CARDINAL ← 1; stampNumberFormat: NumberFormat = [base: 10, zerofill: TRUE, unsigned: TRUE, columns: 5]; ScanForStartOfStamp: PROCEDURE RETURNS [found, eof: BOOLEAN] = BEGIN char, firstStampChar: CHARACTER; charCount: CARDINAL ← 0; firstStampChar ← stampStart[0]; found ← eof ← FALSE; DO char ← inS.get[inS ! StreamError => IF error = StreamAccess THEN GO TO endOfInput]; IF char = compactionMark AND inS.endof[inS] THEN {ReportCompactionMark[]; GO TO endOfInput}; charCount ← charCount + 1; IF char ~= firstStampChar THEN BEGIN outS.put[outS, char]; SELECT charCount FROM < maxPlausibleChars => NULL; IN [maxPlausibleChars..absoluteLimit) => SELECT char FROM IODefs.CR, IN [200C..377C] => EXIT; ENDCASE => NULL; ENDCASE => EXIT; END ELSE FOR i: CARDINAL IN [1..stampStart.length) DO char ← inS.get[inS ! StreamError => IF error = StreamAccess THEN {eof ← TRUE; GO TO noMatch}]; IF char ~= stampStart[i] THEN GO TO noMatch; REPEAT noMatch => BEGIN FOR j: CARDINAL IN [0..i) DO outS.put[outS, stampStart[j]]; ENDLOOP; IF eof THEN BEGIN IF char = compactionMark THEN ReportCompactionMark[] ELSE outS.put[outS, char]; RETURN END; outS.put[outS, char]; END; FINISHED => RETURN[TRUE, FALSE]; ENDLOOP; REPEAT endOfInput => eof ← TRUE; ENDLOOP; END; TryToReadAStamp: PROCEDURE RETURNS [BOOLEAN] = BEGIN stampSize: CARDINAL; ok: BOOLEAN; ReadANumber: PROCEDURE RETURNS [ok: BOOLEAN, value: CARDINAL] = BEGIN value ← 0; THROUGH [0..5) DO char: CHARACTER ← inS.get[inS]; IF char ~IN ['0..'9] THEN RETURN[FALSE, 0]; value ← value * 10 + (char - '0); ENDLOOP; RETURN[TRUE, value] END; IF StreamDefs.GetPosition[inS] + (stampLength-stampStart.length) >= inLength THEN RETURN[FALSE]; [ok, messageSize] ← ReadANumber[]; IF ~ok THEN RETURN[FALSE]; IF inS.get[inS] ~= IODefs.SP THEN RETURN[FALSE]; [ok, stampSize] ← ReadANumber[]; IF ~ok OR stampSize ~= stampLength THEN RETURN[FALSE]; IF inS.get[inS] ~= IODefs.SP THEN RETURN[FALSE]; SELECT deletedFlag ← inS.get[inS] FROM 'D, 'U => NULL; ENDCASE => RETURN[FALSE]; SELECT seenFlag ← inS.get[inS] FROM 'S, 'U => NULL; ENDCASE => RETURN[FALSE]; IF (markChar ← inS.get[inS]) = IODefs.NUL THEN RETURN[FALSE]; IF inS.get[inS] ~= IODefs.CR THEN RETURN[FALSE]; RETURN[TRUE] END; OutputPrototypeStamp: PROCEDURE = BEGIN prototypeFlagsAndMark: STRING = "UUS"L; PutString[outS, stampStart]; PutNumber[outS, 0, stampNumberFormat]; PutChar[outS, IODefs.SP]; PutNumber[outS, stampLength, stampNumberFormat]; PutChar[outS, IODefs.SP]; PutString[outS, prototypeFlagsAndMark]; PutChar[outS, IODefs.CR]; lastStampIsPrototype ← changesMade ← TRUE; ReportBogusStamp[]; END; OutputRealStamp: PROCEDURE = BEGIN PutString[outS, stampStart]; PutNumber[outS, messageSize, stampNumberFormat]; PutChar[outS, IODefs.SP]; PutNumber[outS, stampLength, stampNumberFormat]; PutChar[outS, IODefs.SP]; PutChar[outS, deletedFlag]; PutChar[outS, seenFlag]; PutChar[outS, markChar]; PutChar[outS, IODefs.CR]; lastStampIsPrototype ← FALSE; END; ReportCompactionMark: PROCEDURE = BEGIN CleanupAfterTicks[]; changesMade ← TRUE; IODefs.WriteLine["Compaction mark removed."L]; END; ReportBogusStamp: PROCEDURE = BEGIN CleanupAfterTicks[]; WriteMessageNumber[]; IODefs.WriteLine["reconstructing stamp information."L]; END; ReportSizeChange: PROCEDURE [old, new: CARDINAL] = BEGIN OPEN IODefs; difference: CARDINAL = IF old > new THEN old-new ELSE new-old; CleanupAfterTicks[]; WriteMessageNumber[]; WriteString["existing count was "L]; WriteDecimal[difference]; WriteString[" byte"]; IF difference ~= 1 THEN WriteChar['s]; WriteString[" too "L]; IF old > new THEN WriteLine["long."] ELSE WriteLine["short."]; END; ReportProgress: PROCEDURE = BEGIN OPEN IODefs; IF messageNumber MOD 5 = 0 THEN BEGIN IF ticksSinceLastMessage THEN WriteChar[SP] ELSE ticksSinceLastMessage ← TRUE; WriteDecimal[messageNumber]; END; END; CleanupAfterTicks: PROCEDURE = BEGIN OPEN IODefs; IF ticksSinceLastMessage THEN {WriteChar[CR]; ticksSinceLastMessage ← FALSE}; END; Summarize: PROCEDURE = BEGIN OPEN IODefs; CleanupAfterTicks[]; WriteDecimal[messageNumber]; WriteString[" message"L]; IF messageNumber ~= 1 THEN WriteChar['s]; WriteLine[" processed."L]; END; WriteMessageNumber: PROCEDURE = BEGIN OPEN IODefs; WriteString["Message "L]; WriteDecimal[messageNumber]; WriteString[": "L]; END; inS.reset[inS]; IF inLength < stampStart.length THEN OutputPrototypeStamp[] ELSE BEGIN s: STRING ← [8 --stampStart.length--]; THROUGH [0..stampStart.length) DO StringDefs.AppendChar[s, inS.get[inS]]; ENDLOOP; IF StringDefs.EqualString[s, stampStart] THEN IF TryToReadAStamp[] THEN OutputRealStamp[] ELSE OutputPrototypeStamp[] ELSE {OutputPrototypeStamp[]; inS.reset[inS]}; END; DO OPEN StreamDefs; prevStampStart: StreamPosition ← GetPosition[outS] - stampLength; pos: StreamPosition; charsMoved: CARDINAL; stampFound, eof: BOOLEAN; [stampFound, eof] ← ScanForStartOfStamp[]; charsMoved ← InlineDefs.LowHalf[(pos ← GetPosition[outS]) - prevStampStart]; IF lastStampIsPrototype OR charsMoved ~= messageSize THEN BEGIN changesMade ← TRUE; IF ~lastStampIsPrototype THEN ReportSizeChange[old: messageSize, new: charsMoved]; SetPosition[outS, prevStampStart + stampStart.length]; PutNumber[outS, charsMoved, stampNumberFormat]; SetPosition[outS, pos]; END; IF eof THEN EXIT; ReportProgress[]; messageNumber ← messageNumber + 1; IF stampFound AND TryToReadAStamp[] THEN OutputRealStamp[] ELSE OutputPrototypeStamp[]; ENDLOOP; Summarize[]; END; Finalize: PROCEDURE = BEGIN OPEN IODefs, SegmentDefs; char: CHARACTER; destroyScratchF: BOOLEAN ← TRUE; WriteChar[CR]; IF changesMade THEN BEGIN WriteString["Scavenging complete into "L]; WriteLine[scratchFile]; WriteString["Shall I copy it back to "L]; WriteString[mailFile]; WriteChar['?]; char ← ReadChar[]; SELECT char FROM CR, 'Y, 'y, ESC => BEGIN OPEN StreamDefs; buffer: POINTER ← SystemDefs.AllocatePages[1]; eofPage: PageNumber; eofByte: [0..2*pageSize]; WriteLine[" Yes"L]; CleanupDiskStream[outS]; [eofPage, eofByte] ← GetEndOfFile[scratchF]; inS.reset[outS]; inS.destroy[inS]; SetFileAccess[mailF, writeFileAccess]; inS ← outS; outS ← CreateByteStream[mailF, writeStreamAccess]; WriteString["Copying..."L]; THROUGH [1..eofPage) DO [] ← ReadBlock[inS, buffer, pageSize]; [] ← WriteBlock[outS, buffer, pageSize]; ENDLOOP; THROUGH [0..eofByte) DO outS.put[outS, inS.get[inS]]; ENDLOOP; SystemDefs.FreePages[buffer]; END; ENDCASE => {WriteLine[" No"L]; destroyScratchF ← FALSE}; END ELSE BEGIN WriteString["I couldn't find anything wrong with "L]; WriteLine[mailFile]; END; UnlockFile[mailF]; inS.destroy[inS]; outS.destroy[outS]; UnlockFile[scratchF]; IF destroyScratchF THEN DestroyFile[scratchF] ELSE {WriteString[scratchFile]; WriteLine[" retained."L]}; WriteLine["Done."L]; END; -- Main program IF Initialize[] THEN {Scavenge[]; Finalize[]}; IODefs.WriteChar[IODefs.CR]; END.