-- 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.