---------------------------------------------------------------------------
MODULE Checkpoint;
IMPORT Pathname, Fingerprint, TextRefTbl, TextTextTbl, Text, TextRd, Rd,
FileRd, Wr, FileWr, TextSeq, Thread, OSError;
IMPORT FileInfo, APN AS APN,
FingerprintFmt, TextUtils, TextReadingUtils, PathRepr,
FindExpr, FSFindError, EnvUtils, MsgX, MsgIF;
---------------------------------------------------------------------------
TYPE
CacheElem = OBJECT
fp : REF Fingerprint.T;
attr : AttrSet;
env : TextTextTbl.T
METHODS
init(fp : REF Fingerprint.T; attr : AttrSet) : CacheElem := InitCacheElem;
scan(rd : Rd.T) : CacheElem RAISES {Error} := CacheElemScan;
toText() : TEXT RAISES {Error} := CacheElemToText;
set(a : Attr) := CacheElemSet;
isSet(a : Attr) : BOOLEAN := CacheElemIsSet;
clear(a : Attr) := CacheElemClear;
clearAll() := CacheElemClearAll;
scanEnv(rd : Rd.T) : TEXT RAISES {Error} := CacheElemScanEnv;
setVal(name, val : TEXT) := CacheElemSetVal;
getVal(name : TEXT) : TEXT := CacheElemGetVal;
delVal(name : TEXT) := CacheElemDelVal;
END;
---------------------------------------------------------------------------
PROCEDURE InitCacheElem(self : CacheElem;
fp : REF Fingerprint.T; attr : AttrSet) : CacheElem =
BEGIN
self.fp := fp;
self.attr := attr;
self.env := NIL;
RETURN self;
END InitCacheElem;
---------------------------------------------------------------------------
PROCEDURE CacheElemScan(self : CacheElem; rd : Rd.T) : CacheElem
RAISES {Error} =
VAR
fpt, opt : TEXT;
BEGIN
TRY
fpt := TextReadingUtils.GetTokenOrString(rd);
opt := TextReadingUtils.GetTokenOrString(rd);
self.fp := NEW(REF Fingerprint.T);
IF NOT FingerprintFmt.Scan(fpt, self.fp^) THEN
RAISE Error("invalid fingerprint: " & fpt);
END;
AttrFromText(opt, self.attr);
EXCEPT
Rd.EndOfFile => RAISE Error("unexpected end of file");
| Rd.Failure => RAISE Error("text reader failure");
| Thread.Alerted => RAISE Error("interrupted");
END;
RETURN self;
END CacheElemScan;
---------------------------------------------------------------------------
PROCEDURE CacheElemScanEnv(self : CacheElem; rd : Rd.T) : TEXT
RAISES {Error} =
VAR
tok : TEXT;
BEGIN
TRY
tok := TextReadingUtils.GetTokenOrString(rd);
IF Text.Equal(tok, "(@env-start") THEN
self.env := EnvUtils.FromRd(rd, skipStart := TRUE);
tok := NIL;
END;
EXCEPT
Rd.EndOfFile => tok := NIL; (* skip, may be correct *)
| Rd.Failure => RAISE Error("text reader failure");
| Thread.Alerted => RAISE Error("interrupted");
| EnvUtils.Error(e) => RAISE Error(e);
END;
RETURN tok;
END CacheElemScanEnv;
---------------------------------------------------------------------------
PROCEDURE CacheElemToText(self : CacheElem) : TEXT RAISES {Error} =
VAR env := "";
BEGIN
IF self.env # NIL THEN
TRY
env := B & EnvUtils.ToText(self.env);
EXCEPT ELSE
RAISE Error("cannot convert checkpoint environment");
END;
END;
RETURN FingerprintFmt.Hex(self.fp^) & B & AttrToText(self.attr) & env;
END CacheElemToText;
---------------------------------------------------------------------------
PROCEDURE CacheElemSet(self : CacheElem; a : Attr) =
BEGIN
self.attr := self.attr + AttrSet{a};
END CacheElemSet;
---------------------------------------------------------------------------
PROCEDURE CacheElemIsSet(self : CacheElem; a : Attr) : BOOLEAN =
BEGIN
RETURN a IN self.attr;
END CacheElemIsSet;
---------------------------------------------------------------------------
PROCEDURE CacheElemClear(self : CacheElem; a : Attr) =
BEGIN
self.attr := self.attr - AttrSet{a};
END CacheElemClear;
---------------------------------------------------------------------------
PROCEDURE CacheElemClearAll(self : CacheElem) =
BEGIN
self.attr := AttrSet{};
END CacheElemClearAll;
---------------------------------------------------------------------------
PROCEDURE NewCacheElem(fp : REF Fingerprint.T; attr : AttrSet) : CacheElem =
BEGIN
RETURN NEW(CacheElem).init(fp, attr);
END NewCacheElem;
---------------------------------------------------------------------------
PROCEDURE CacheElemSetVal(self : CacheElem; name, val : TEXT) =
BEGIN
IF self.env = NIL THEN
self.env := NEW(TextTextTbl.Default).init();
END;
EVAL self.env.put(name, val);
END CacheElemSetVal;
---------------------------------------------------------------------------
PROCEDURE CacheElemGetVal(self : CacheElem; name : TEXT) : TEXT =
VAR val : TEXT;
BEGIN
IF self.env = NIL THEN
RETURN NIL;
END;
IF self.env.get(name, val) THEN
RETURN val;
ELSE
RETURN NIL;
END;
END CacheElemGetVal;
---------------------------------------------------------------------------
PROCEDURE CacheElemDelVal(self : CacheElem; name : TEXT) =
VAR val : TEXT;
BEGIN
IF self.env = NIL THEN
RETURN;
END;
EVAL self.env.delete(name, val);
END CacheElemDelVal;
---------------------------------------------------------------------------
REVEAL
T = Public BRANDED " Checkpoint 0.0" OBJECT
cache : FileInfo.T;
roots : TextRefTbl.T;
msgif : MsgIF.T;
OVERRIDES
init := Init;
addDir := AddDir;
delDir := DelDir;
dirs := Dirs;
fingerprint := GetFingerprint;
update := Update;
diff := Diff;
selectByAttr := SelectByAttr;
toText := ToText;
fromRd := FromRd;
fromText := FromText;
toFile := ToFile;
fromFile := FromFile;
getAttr := GetAttr;
setAttr := SetAttr;
getVal := GetVal;
setVal := SetVal;
delVal := DelVal;
END;
---------------------------------------------------------------------------
PROCEDURE Init(self : T; cache : FileInfo.T; msgif : MsgIF.T := NIL) : T =
BEGIN
<* ASSERT cache # NIL *>
self.msgif := msgif;
self.cache := cache;
self.roots := NEW(TextRefTbl.Default).init();
RETURN self;
END Init;
---------------------------------------------------------------------------
PROCEDURE AddDir(self : T; dir : Pathname.T; fp : REF Fingerprint.T := NIL)
RAISES {Error} =
BEGIN
<* ASSERT self.cache # NIL *>
dir := PathRepr.Native(dir);
IF fp = NIL THEN
WITH dirp = APN.New(dir) DO
fp := self.cache.fingerprint(dirp, ignoreDirExpr, ignoreFileExpr);
IF fp = NIL THEN
MsgX.V(self.msgif, "scanning directory " & dir);
self.cache.updateRec(dirp, NIL, NIL,
skipDirExpr, skipFileExpr);
fp := self.cache.fingerprint(dirp, ignoreDirExpr, ignoreFileExpr);
IF fp = NIL THEN
RAISE Error("cannot compute fingerprint for directory " & dir);
END;
END;
END;
END;
<* ASSERT fp # NIL *>
WITH ce = NewCacheElem(fp, AttrSet{}) DO
EVAL self.roots.put(dir, ce);
END;
END AddDir;
---------------------------------------------------------------------------
PROCEDURE DelDir(self : T; dir : Pathname.T) =
VAR ce : REFANY;
BEGIN
<* ASSERT self.roots # NIL *>
dir := PathRepr.Native(dir);
EVAL self.roots.delete(dir, ce);
END DelDir;
---------------------------------------------------------------------------
PROCEDURE Dirs(self : T) : TextSeq.T =
VAR
iter := self.roots.iterate();
fn : TEXT;
ref : REFANY;
res := NEW(TextSeq.T).init();
BEGIN
WHILE iter.next(fn, ref) DO
res.addhi(fn);
END;
RETURN res;
END Dirs;
---------------------------------------------------------------------------
PROCEDURE GetFingerprint(self : T; dir : Pathname.T) : REF Fingerprint.T =
VAR ref : REFANY;
BEGIN
<* ASSERT self.roots # NIL *>
dir := PathRepr.Native(dir);
IF self.roots.get(dir, ref) THEN
RETURN NARROW(ref, CacheElem).fp;
ELSE
RETURN NIL;
END;
END GetFingerprint;
---------------------------------------------------------------------------
PROCEDURE Update(self : T; dir : Pathname.T := NIL; missingOnly := FALSE)
RAISES {Error} =
VAR
iter := self.roots.iterate();
fn : TEXT;
ref : REFANY;
errs := "";
PROCEDURE UpdateOne(dir : Pathname.T; ref : REFANY) =
VAR fp : REF Fingerprint.T;
BEGIN
dir := PathRepr.Native(dir);
WITH dirp = APN.New(dir) DO
MsgX.V(self.msgif, "scanning directory " & dir);
IF missingOnly THEN
fp := self.cache.fingerprint(dirp, ignoreDirExpr, ignoreFileExpr);
END;
IF NOT missingOnly OR fp = NIL THEN
self.cache.updateRec(dirp, NIL, NIL,
skipDirExpr, skipFileExpr);
fp := self.cache.fingerprint(dirp, ignoreDirExpr, ignoreFileExpr);
END;
IF fp = NIL THEN
errs := errs & B & dir;
ELSE
NARROW(ref, CacheElem).fp := fp;
EVAL self.roots.put(dir, ref);
END;
END;
END UpdateOne;
BEGIN (* Update *)
IF dir = NIL THEN
WHILE iter.next(fn, ref) DO
UpdateOne(fn, ref);
END;
ELSE
IF self.roots.get(dir, ref) THEN
UpdateOne(dir, ref);
ELSE
AddDir(self, dir);
END;
END;
IF NOT Text.Empty(errs) THEN
RAISE Error("cannot compute fingerprint for the following " &
"directories: " & errs);
END;
END Update;
---------------------------------------------------------------------------
PROCEDURE Diff(self : T; cp : T) : TextSeq.T =
VAR
iter := self.roots.iterate();
fn : TEXT;
ref : REFANY;
fp : REF Fingerprint.T;
ce : CacheElem;
res := NEW(TextSeq.T).init();
BEGIN
WHILE iter.next(fn, ref) DO
fp := cp.fingerprint(fn);
ce := NARROW(ref, CacheElem);
IF fp = NIL OR fp^ # ce.fp^ THEN
res.addhi(fn);
ce.attr := ce.attr + AttrSet{Attr.Changed};
ELSE
ce.attr := ce.attr - AttrSet{Attr.Changed};
END;
END;
RETURN res;
END Diff;
---------------------------------------------------------------------------
PROCEDURE SelectByAttr(self : T; attr : AttrSet) : TextSeq.T =
VAR
iter := self.roots.iterate();
fn : TEXT;
ref : REFANY;
ce : CacheElem;
res := NEW(TextSeq.T).init();
BEGIN
WHILE iter.next(fn, ref) DO
ce := NARROW(ref, CacheElem);
IF ce.attr * attr # AttrSet{} THEN
res.addhi(fn);
END;
END;
RETURN res;
END SelectByAttr;
---------------------------------------------------------------------------
PROCEDURE ToText(self : T; ) : TEXT RAISES {Error} =
VAR
iter := self.roots.iterate();
fn : TEXT;
ref : REFANY;
ce : CacheElem;
res := "";
BEGIN
WHILE iter.next(fn, ref) DO
IF ref # NIL THEN
ce := NARROW(ref, CacheElem);
WITH cetext = ce.toText() DO
res := res & Q & fn & Q & B & cetext & "\n";
END;
END;
END;
RETURN res;
END ToText;
---------------------------------------------------------------------------
PROCEDURE FromRd(self : T; rd : Rd.T) RAISES {Error} =
VAR
fn : TEXT := NIL;
ce : CacheElem;
BEGIN
<* ASSERT self.cache # NIL *>
<* ASSERT self.roots # NIL *>
TRY
TRY
WHILE NOT Rd.EOF(rd) DO
IF fn = NIL THEN
fn := TextReadingUtils.GetTokenOrString(rd);
END;
fn := PathRepr.Native(fn);
ce := NEW(CacheElem).scan(rd);
EVAL self.roots.put(fn, ce);
fn := ce.scanEnv(rd);
END;
EXCEPT
Rd.EndOfFile => (* skip *)
| Rd.Failure => RAISE Error("text reader failure");
| Thread.Alerted => RAISE Error("interrupted");
END;
FINALLY
IF rd # NIL THEN
TRY Rd.Close(rd) EXCEPT ELSE END;
END;
END;
END FromRd;
---------------------------------------------------------------------------
PROCEDURE FromText(self : T; t : TEXT) RAISES {Error} =
VAR
rd := TextRd.New(t);
BEGIN
<* ASSERT self.cache # NIL *>
<* ASSERT self.roots # NIL *>
FromRd(self, rd);
END FromText;
---------------------------------------------------------------------------
PROCEDURE ToFile(self : T; fn : Pathname.T) RAISES {Error} =
VAR
iter := self.roots.iterate();
dir : TEXT;
ref : REFANY;
ce : CacheElem;
line := "";
wr : Wr.T := NIL;
BEGIN
TRY
wr := FileWr.Open(fn);
EXCEPT
OSError.E => RAISE Error("cannot open file " & fn);
END;
TRY
TRY
WHILE iter.next(dir, ref) DO
IF ref # NIL THEN
ce := NARROW(ref, CacheElem);
WITH cetext = ce.toText() DO
line := Q & dir & Q & B & cetext & "\n";
Wr.PutText(wr, line);
END;
END;
END;
EXCEPT
Wr.Failure => RAISE Error("write failed on file " & fn);
| Thread.Alerted => RAISE Error("interrupted writing file " & fn);
END;
FINALLY
IF wr # NIL THEN
TRY Wr.Close(wr) EXCEPT ELSE END;
END;
END;
END ToFile;
---------------------------------------------------------------------------
PROCEDURE FromFile(self : T; fn : Pathname.T) RAISES {Error} =
VAR
rd : Rd.T := NIL;
BEGIN
<* ASSERT self.cache # NIL *>
<* ASSERT self.roots # NIL *>
TRY
rd := FileRd.Open(fn);
EXCEPT
OSError.E => RAISE Error("cannot open file " & fn);
END;
FromRd(self, rd);
END FromFile;
---------------------------------------------------------------------------
PROCEDURE GetAttr(self : T; dir : Pathname.T) : AttrSet RAISES {Error} =
VAR
ref : REFANY;
ce : CacheElem;
BEGIN
<* ASSERT self.roots # NIL *>
dir := PathRepr.Native(dir);
IF self.roots.get(dir, ref) THEN
ce := NARROW(ref, CacheElem);
RETURN ce.attr;
ELSE
RAISE Error("cache miss: " & dir);
END;
END GetAttr;
---------------------------------------------------------------------------
PROCEDURE SetAttr(self : T; dir : Pathname.T; attr : AttrSet) RAISES {Error} =
VAR
ref : REFANY;
ce : CacheElem;
BEGIN
<* ASSERT self.roots # NIL *>
dir := PathRepr.Native(dir);
IF self.roots.get(dir, ref) THEN
ce := NARROW(ref, CacheElem);
ce.attr := attr;
ELSE
RAISE Error("cache miss: " & dir);
END;
END SetAttr;
---------------------------------------------------------------------------
PROCEDURE SetVal(self : T; dir : Pathname.T; name, val : TEXT) RAISES {Error} =
VAR
ref : REFANY;
ce : CacheElem;
BEGIN
<* ASSERT self.roots # NIL *>
dir := PathRepr.Native(dir);
IF self.roots.get(dir, ref) THEN
ce := NARROW(ref, CacheElem);
ce.setVal(name, val);
ELSE
RAISE Error("cache miss: " & dir);
END;
END SetVal;
---------------------------------------------------------------------------
PROCEDURE GetVal(self : T; dir : Pathname.T; name : TEXT) : TEXT
RAISES {Error} =
VAR
ref : REFANY;
ce : CacheElem;
BEGIN
<* ASSERT self.roots # NIL *>
dir := PathRepr.Native(dir);
IF self.roots.get(dir, ref) THEN
ce := NARROW(ref, CacheElem);
RETURN ce.getVal(name);
ELSE
RAISE Error("cache miss: " & dir);
END;
END GetVal;
---------------------------------------------------------------------------
PROCEDURE DelVal(self : T; dir : Pathname.T; name : TEXT) RAISES {Error} =
VAR
ref : REFANY;
ce : CacheElem;
BEGIN
<* ASSERT self.roots # NIL *>
dir := PathRepr.Native(dir);
IF self.roots.get(dir, ref) THEN
ce := NARROW(ref, CacheElem);
ce.delVal(name);
ELSE
RAISE Error("cache miss: " & dir);
END;
END DelVal;
---------------------------------------------------------------------------
PROCEDURE AttrToText(attr : AttrSet) : TEXT =
VAR
res := Q;
first := TRUE;
BEGIN
FOR a := FIRST(Attr) TO LAST(Attr) DO
IF a IN attr THEN
IF first THEN
res := res & AttrRepr[a];
first := FALSE;
ELSE
res := res & B & AttrRepr[a];
END;
END;
END;
RETURN res & Q;
END AttrToText;
---------------------------------------------------------------------------
PROCEDURE AttrFromText(t : TEXT; VAR attr : AttrSet) RAISES {Error} =
PROCEDURE OneFromText(one : TEXT) : Attr =
BEGIN
FOR a := FIRST(Attr) TO LAST(Attr) DO
IF Text.Equal(one, AttrRepr[a]) THEN
RETURN a;
END;
END;
RETURN Attr.None;
END OneFromText;
VAR
seq := TextUtils.Split(t, B);
BEGIN
attr := AttrSet{};
FOR i := 0 TO seq.size() - 1 DO
WITH elem = TextUtils.Compress(seq.get(i)) DO
IF NOT Text.Empty(elem) THEN
WITH a = OneFromText(elem) DO
IF a # Attr.None THEN
attr := attr + AttrSet{a};
ELSE
RAISE Error("unknown attribute: " & elem);
END;
END;
END;
END;
END;
END AttrFromText;
---------------------------------------------------------------------------
PROCEDURE New(c : FileInfo.T; msgif : MsgIF.T := NIL) : T =
BEGIN
RETURN NEW(T).init(c, msgif);
END New;
---------------------------------------------------------------------------
PROCEDURE DefineIgnorePatterns(
cacheIgnoreDirs : TEXT := NIL;
cacheIgnoreFiles : TEXT := NIL;
fingerprintIgnoreDirs : TEXT := NIL;
fingerprintIgnoreFiles : TEXT := NIL) RAISES {Error} =
BEGIN
TRY
IF cacheIgnoreDirs # NIL THEN
skipDirExpr := FindExpr.New(cacheIgnoreDirs);
END;
EXCEPT
FSFindError.E(e) =>
RAISE Error("error in directory ignore pattern for file cache: " & e);
END;
TRY
IF cacheIgnoreFiles # NIL THEN
skipFileExpr := FindExpr.New(cacheIgnoreFiles);
END;
EXCEPT
FSFindError.E(e) =>
RAISE Error("error in file ignore pattern for file cache: " & e);
END;
TRY
IF fingerprintIgnoreDirs # NIL THEN
ignoreDirExpr := FindExpr.New(fingerprintIgnoreDirs);
END;
EXCEPT
FSFindError.E(e) =>
RAISE Error("error in directory ignore pattern for fingerprints: " & e);
END;
TRY
IF fingerprintIgnoreFiles # NIL THEN
ignoreFileExpr := FindExpr.New(fingerprintIgnoreFiles);
END;
EXCEPT
FSFindError.E(e) =>
RAISE Error("error in file ignore pattern for fingerprints: " & e);
END;
END DefineIgnorePatterns;
---------------------------------------------------------------------------
CONST
B = " ";
Q = "\"";
IgnDir = "tmp or temp";
IgnFile = "\"PkgCDT\" or \"PkgCRT\" or \"PkgCT\" or \"*~\" or " &
"\"*.bak\" or \"*.tmp\" or \"*.temp\" or \"PkgErr\"";
SkipDir = IgnDir;
SkipFile = IgnFile;
BEGIN
ignoreDirExpr := FindExpr.New(IgnDir); <* NOWARN *>
ignoreFileExpr := FindExpr.New(IgnFile); <* NOWARN *>
skipDirExpr := FindExpr.New(SkipDir); <* NOWARN *>
skipFileExpr := FindExpr.New(SkipFile); <* NOWARN *>
END Checkpoint.