Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package perl-Cavil-Matcher for 
openSUSE:Factory checked in at 2026-08-06 16:27:06
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/perl-Cavil-Matcher (Old)
 and      /work/SRC/openSUSE:Factory/.perl-Cavil-Matcher.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "perl-Cavil-Matcher"

Thu Aug  6 16:27:06 2026 rev:2 rq:1370033 version:1.10.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/perl-Cavil-Matcher/perl-Cavil-Matcher.changes    
2026-07-22 19:08:53.928854130 +0200
+++ 
/work/SRC/openSUSE:Factory/.perl-Cavil-Matcher.new.16738/perl-Cavil-Matcher.changes
 2026-08-06 16:28:59.290628970 +0200
@@ -1,0 +2,21 @@
+Wed Aug  5 07:49:19 UTC 2026 - Tina Müller <[email protected]>
+
+- updated to 1.10.0 (1.01)
+   see /usr/share/doc/packages/perl-Cavil-Matcher/Changes
+
+  1.01   2026-08-04
+          - Big indexing speedup: the scan path (load/attach) no longer 
re-checksums a segment's whole
+            payload on every open. A compiled segment is an immutable, 
atomically-published cache that is
+            already CRC-checked when written, so re-CRCing hundreds of 
megabytes in every forked index job
+            was almost the entire cost of a load (~97% in a 160 MB corpus: 
~293ms -> ~8ms). Structural
+            validation still always runs, so bad segments are rejected and 
scanning stays memory-safe;
+            only per-scan detection of on-disk bit-rot is dropped.
+          - CRC is now verified where it is meaningful: once at publish 
(dump() fully verifies before the
+            atomic rename) and on demand via the new Engine verify($file) 
method (an fsck for a cache dir).
+          - Same treatment for the closest-match "bag of patterns" cache 
(Bag::load, used by analyze): the
+            load path trusts the payload CRC (the record parse still 
bounds-checks every read), with a new
+            Bag verify($file) method and a publish-time check.
+          - No on-disk format change: existing segments load as-is, no 
reindex, and old/new engines
+            interoperate on the same files (in-place upgrade is seamless).
+
+-------------------------------------------------------------------

Old:
----
  Cavil-Matcher-1.00.tar.gz

New:
----
  Cavil-Matcher-1.01.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ perl-Cavil-Matcher.spec ++++++
--- /var/tmp/diff_new_pack.LSAZqV/_old  2026-08-06 16:29:00.298664154 +0200
+++ /var/tmp/diff_new_pack.LSAZqV/_new  2026-08-06 16:29:00.298664154 +0200
@@ -18,14 +18,14 @@
 
 %define cpan_name Cavil-Matcher
 Name:           perl-Cavil-Matcher
-Version:        1.0.0
+Version:        1.10.0
 Release:        0
-# 1.00 -> normalize -> 1.0.0
-%define cpan_version 1.00
+# 1.01 -> normalize -> 1.10.0
+%define cpan_version 1.01
 License:        GPL-1.0-or-later
 Summary:        Next-generation license pattern matcher for Cavil
 URL:            https://metacpan.org/release/%{cpan_name}
-Source0:        Cavil-Matcher-1.00.tar.gz
+Source0:        
https://cpan.metacpan.org/authors/id/K/KR/KRAIH/%{cpan_name}-%{cpan_version}.tar.gz
 Source1:        cpanspec.yml
 Source100:      README.md
 BuildRequires:  perl

++++++ Cavil-Matcher-1.00.tar.gz -> Cavil-Matcher-1.01.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/Changes 
new/Cavil-Matcher-1.01/Changes
--- old/Cavil-Matcher-1.00/Changes      2026-07-22 14:59:29.258187138 +0200
+++ new/Cavil-Matcher-1.01/Changes      2026-08-04 20:28:44.956802618 +0200
@@ -1,5 +1,20 @@
 Revision history for Cavil::Matcher
 
+1.01   2026-08-04
+        - Big indexing speedup: the scan path (load/attach) no longer 
re-checksums a segment's whole
+          payload on every open. A compiled segment is an immutable, 
atomically-published cache that is
+          already CRC-checked when written, so re-CRCing hundreds of megabytes 
in every forked index job
+          was almost the entire cost of a load (~97% in a 160 MB corpus: 
~293ms -> ~8ms). Structural
+          validation still always runs, so bad segments are rejected and 
scanning stays memory-safe;
+          only per-scan detection of on-disk bit-rot is dropped.
+        - CRC is now verified where it is meaningful: once at publish (dump() 
fully verifies before the
+          atomic rename) and on demand via the new Engine verify($file) method 
(an fsck for a cache dir).
+        - Same treatment for the closest-match "bag of patterns" cache 
(Bag::load, used by analyze): the
+          load path trusts the payload CRC (the record parse still 
bounds-checks every read), with a new
+          Bag verify($file) method and a publish-time check.
+        - No on-disk format change: existing segments load as-is, no reindex, 
and old/new engines
+          interoperate on the same files (in-place upgrade is seamless).
+
 1.00   2026-07-22
         - Initial release: the next-generation license pattern matcher for 
Cavil.
         - Segmented, incrementally-updatable index: adding or removing a 
pattern no longer
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/META.json 
new/Cavil-Matcher-1.01/META.json
--- old/Cavil-Matcher-1.00/META.json    2026-07-22 15:00:53.330011255 +0200
+++ new/Cavil-Matcher-1.01/META.json    2026-08-04 20:33:35.142118638 +0200
@@ -4,7 +4,7 @@
       "Sebastian Riedel <[email protected]>"
    ],
    "dynamic_config" : 0,
-   "generated_by" : "ExtUtils::MakeMaker version 7.76, CPAN::Meta::Converter 
version 2.150010",
+   "generated_by" : "ExtUtils::MakeMaker version 7.78, CPAN::Meta::Converter 
version 2.150013",
    "license" : [
       "gpl_2"
    ],
@@ -60,6 +60,6 @@
          "web" : "https://github.com/openSUSE/cavil-matcher";
       }
    },
-   "version" : "1.00",
+   "version" : "1.01",
    "x_serialization_backend" : "JSON::PP version 4.16"
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/META.yml 
new/Cavil-Matcher-1.01/META.yml
--- old/Cavil-Matcher-1.00/META.yml     2026-07-22 15:00:53.296892369 +0200
+++ new/Cavil-Matcher-1.01/META.yml     2026-08-04 20:33:35.110368413 +0200
@@ -9,7 +9,7 @@
 configure_requires:
   ExtUtils::MakeMaker: '7.12'
 dynamic_config: 0
-generated_by: 'ExtUtils::MakeMaker version 7.76, CPAN::Meta::Converter version 
2.150010'
+generated_by: 'ExtUtils::MakeMaker version 7.78, CPAN::Meta::Converter version 
2.150013'
 license: gpl
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
@@ -28,5 +28,5 @@
   bugtracker: https://github.com/openSUSE/cavil-matcher/issues
   license: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
   repository: https://github.com/openSUSE/cavil-matcher.git
-version: '1.00'
+version: '1.01'
 x_serialization_backend: 'CPAN::Meta::YAML version 0.020'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/Matcher.xs 
new/Cavil-Matcher-1.01/Matcher.xs
--- old/Cavil-Matcher-1.00/Matcher.xs   2026-07-21 21:43:04.197090308 +0200
+++ new/Cavil-Matcher-1.01/Matcher.xs   2026-08-04 20:27:15.064991701 +0200
@@ -91,6 +91,12 @@
   OUTPUT:
     RETVAL
 
+int verify(Cavil::Matcher::Engine self, const char *filename)
+  CODE:
+    RETVAL = matcher_verify(self, filename);
+  OUTPUT:
+    RETVAL
+
 void set_tombstones(Cavil::Matcher::Engine self, AV *ids)
   CODE:
     matcher_set_tombstones(self, ids);
@@ -152,3 +158,9 @@
     RETVAL = bag_load(self, filename);
   OUTPUT:
     RETVAL
+
+int verify(Cavil::Matcher::Bag self, const char *filename)
+  CODE:
+    RETVAL = bag_verify(self, filename);
+  OUTPUT:
+    RETVAL
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/docs/Architecture.md 
new/Cavil-Matcher-1.01/docs/Architecture.md
--- old/Cavil-Matcher-1.00/docs/Architecture.md 2026-07-21 21:08:46.064115638 
+0200
+++ new/Cavil-Matcher-1.01/docs/Architecture.md 2026-08-04 20:20:04.757907627 
+0200
@@ -112,12 +112,21 @@
 ## The on-disk format
 
 Each segment file begins with a header identifying it, stating its format 
version, and carrying a checksum of
-everything that follows. When a segment is opened, all of this is verified, 
and every internal reference in the
-file is bounds-checked before it is ever used. A file that is truncated, 
corrupted, of the wrong version, or
+everything that follows. Opening a segment always validates its **structure** 
— the header fields, the size,
+and every internal reference — so a file that is truncated, of the wrong 
version, structurally impossible, or
 simply not a segment at all is rejected cleanly; it is never partially trusted 
and never able to send the
-scanner off the end of the data. The manifest additionally records a checksum 
for each segment, so a segment
-that has been damaged or swapped underneath a running system is detected and 
skipped rather than used. This
-validated, versioned format is the deliberate replacement for the old engine's 
headerless, unchecked file.
+scanner off the end of the data. This structural validation is cheap and 
always on, so memory safety never
+depends on the checksum.
+
+The whole-payload **checksum** is a separate, corruption-detection concern, 
and it is deliberately *not*
+recomputed on the hot scan path. A segment is Cavil's own derived cache: it is 
checksummed when compiled,
+published atomically (written to the side and renamed into place), and 
thereafter immutable and regenerable
+from the database. Re-checksumming a multi-hundred-megabyte payload on every 
one of the thousands of indexing
+opens — where the mmap already shares one physical copy — is almost the entire 
cost of a load (measured at
+~97%), and it guards against a corruption that atomic publishing already 
prevents. So the CRC is verified where
+it is meaningful: once when the segment is written, and on demand through the 
engine's `verify` entry point (an
+fsck for operators). The manifest additionally records a checksum for each 
segment for that on-demand check.
+This validated, versioned format is the deliberate replacement for the old 
engine's headerless, unchecked file.
 
 ## Robustness
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/lib/Cavil/Matcher.pm 
new/Cavil-Matcher-1.01/lib/Cavil/Matcher.pm
--- old/Cavil-Matcher-1.00/lib/Cavil/Matcher.pm 2026-07-22 14:20:47.936531607 
+0200
+++ new/Cavil-Matcher-1.01/lib/Cavil/Matcher.pm 2026-08-04 20:28:57.000000000 
+0200
@@ -6,7 +6,7 @@
 use strict;
 use warnings;
 
-our $VERSION = '1.00';
+our $VERSION = '1.01';
 
 require XSLoader;
 XSLoader::load('Cavil::Matcher', $VERSION);
@@ -142,12 +142,25 @@
 =item dump($file) / load($file)
 
 Write the in-memory patterns to a compiled segment file / replace the engine's 
state with a single mmapped
-segment file.
+segment file. C<dump> fully verifies the compiled segment before publishing 
it, so a bad segment is never
+written.
+
+C<load> (like C<attach>) validates the segment's structure but does not 
re-checksum its whole payload: a
+compiled segment is an immutable, already-verified cache, and re-checksumming 
hundreds of megabytes on every
+open is almost the entire cost of a load. Structure is always validated, so a 
malformed file is still rejected
+and scanning is always memory-safe; only on-disk corruption of an otherwise 
well-formed file goes undetected
+here (use L</verify> for that).
 
 =item attach($file)
 
 Memory-map an additional compiled segment into the active set. Returns false 
(without dying) on a missing or
-invalid file.
+structurally invalid file. Trusts the payload checksum like L</load>.
+
+=item verify($file)
+
+Fully check a segment file on disk — structure B<and> whole-payload checksum — 
and return true iff it is
+intact. This is the corruption check that the scan path (C<load>/C<attach>) 
deliberately skips for speed; run
+it on demand (an fsck for a cache directory) when you need to prove a segment 
has not rotted on disk.
 
 =item set_tombstones(\@pattern_ids)
 
@@ -169,8 +182,10 @@
 =head1 Cavil::Matcher::Bag
 
 tf-idf closest-match model from L</init_bag_of_patterns>: 
C<set_patterns(\%id_to_text)>,
-C<best_for($text, $count)>, C<dump($file)> and C<load($file)>. C<dump> and 
C<load> return true on
-success; a failed C<load> (missing/truncated file) leaves the model unchanged.
+C<best_for($text, $count)>, C<dump($file)>, C<load($file)> and 
C<verify($file)>. C<dump> and C<load>
+return true on success; a failed C<load> (missing/truncated/malformed file) 
leaves the model unchanged.
+Like the engine, C<load> validates structure but trusts the payload checksum 
of this immutable,
+already-verified cache; C<verify> is the on-demand full check (structure 
B<and> CRC) for an fsck.
 
 =head1 SEE ALSO
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/bag.cc 
new/Cavil-Matcher-1.01/src/bag.cc
--- old/Cavil-Matcher-1.00/src/bag.cc   2026-07-21 15:26:34.843863847 +0200
+++ new/Cavil-Matcher-1.01/src/bag.cc   2026-08-04 20:26:55.145033312 +0200
@@ -166,6 +166,9 @@
   bool ok = fwrite(&h, sizeof(h), 1, file) == 1;
   if (ok && !payload.empty()) ok = fwrite(payload.data(), payload.size(), 1, 
file) == 1;
   if (fclose(file) != 0) ok = false;
+  // Prove integrity once, at publish: fully re-read and check the temp (CRC + 
parse) before it becomes
+  // the live cache, so the load path can trust it without re-checksumming on 
every open.
+  if (ok && !verify(tmp)) ok = false;
   if (ok && rename(tmp.c_str(), path.c_str()) != 0) ok = false;
   if (!ok) remove(tmp.c_str());    // don't leave a partial temp behind
   return ok;
@@ -193,7 +196,14 @@
 };
 }    // namespace
 
-bool Bag::load(const std::string& path) {
+bool Bag::verify(const std::string& path) const {
+  // Full integrity check (CRC + parse) of a bag file, for an explicit fsck. 
The load path trusts the CRC
+  // of the immutable published cache; this is how it is proven on demand.
+  Bag tmp;
+  return tmp.load(path, /*verify_crc=*/true);
+}
+
+bool Bag::load(const std::string& path, bool verify_crc) {
   FILE* file = fopen(path.c_str(), "rb");
   if (!file) return false;
 
@@ -225,7 +235,9 @@
 
   const char* payload     = buf.data() + sizeof(BagHeader);
   size_t      payload_len = buf.size() - sizeof(BagHeader);
-  if (cavil_crc32(payload, payload_len) != h.crc32) return false;
+  // Corruption detection over the whole payload. Skipped on the trusted load 
path (see the header doc):
+  // the record parse below bounds-checks every read, so a malformed bag is 
rejected either way.
+  if (verify_crc && cavil_crc32(payload, payload_len) != h.crc32) return false;
 
   // Reject counts that could not possibly fit in the payload before 
allocating anything.
   if (h.idf_count > payload_len / 16) return false;             // each idf 
entry is 16 bytes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/bag.h 
new/Cavil-Matcher-1.01/src/bag.h
--- old/Cavil-Matcher-1.00/src/bag.h    2026-07-21 02:38:02.943372287 +0200
+++ new/Cavil-Matcher-1.01/src/bag.h    2026-08-04 20:26:33.941077604 +0200
@@ -24,7 +24,12 @@
   void             set_patterns(const std::vector<std::pair<uint64_t, 
std::string>>& patterns);
   std::vector<Hit> best_for(const std::string& snippet, unsigned int count) 
const;
   bool             dump(const std::string& path) const;
-  bool             load(const std::string& path);
+  // verify_crc controls only the whole-payload CRC32. It defaults to true; 
the load path (bag_load)
+  // passes false. Parsing the records already bounds-checks every read and 
rejects a malformed file, so
+  // skipping the CRC on load costs no safety - only per-load detection of 
on-disk bit-rot, which the
+  // publish-time check and verify() cover. The bag is Cavil's own immutable, 
atomically-published cache.
+  bool             load(const std::string& path, bool verify_crc = true);
+  bool             verify(const std::string& path) const;    // full CRC + 
parse check of a bag file
 
 private:
   struct TfIdf {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/bindings.cc 
new/Cavil-Matcher-1.01/src/bindings.cc
--- old/Cavil-Matcher-1.00/src/bindings.cc      2026-07-21 21:59:06.236314690 
+0200
+++ new/Cavil-Matcher-1.01/src/bindings.cc      2026-08-04 20:27:02.193018589 
+0200
@@ -265,6 +265,7 @@
 int matcher_dump(Matcher* m, const char* filename) { return m->dump(filename) 
? 1 : 0; }
 int matcher_load(Matcher* m, const char* filename) { return m->load(filename) 
? 1 : 0; }
 int matcher_attach(Matcher* m, const char* filename) { return 
m->attach(filename) ? 1 : 0; }
+int matcher_verify(Matcher* m, const char* filename) { return 
m->verify(filename) ? 1 : 0; }
 
 void matcher_set_tombstones(Matcher* m, AV* ids) {
   dTHX;
@@ -349,4 +350,5 @@
 }
 
 int  bag_dump(Bag* b, const char* filename) { return b->dump(filename) ? 1 : 
0; }
-int  bag_load(Bag* b, const char* filename) { return b->load(filename) ? 1 : 
0; }
+int  bag_load(Bag* b, const char* filename) { return b->load(filename, 
/*verify_crc=*/false) ? 1 : 0; }
+int  bag_verify(Bag* b, const char* filename) { return b->verify(filename) ? 1 
: 0; }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/bindings.h 
new/Cavil-Matcher-1.01/src/bindings.h
--- old/Cavil-Matcher-1.00/src/bindings.h       2026-07-21 21:42:58.359155899 
+0200
+++ new/Cavil-Matcher-1.01/src/bindings.h       2026-08-04 20:27:06.717009139 
+0200
@@ -35,6 +35,7 @@
 int      matcher_dump(Matcher* m, const char* filename);
 int      matcher_load(Matcher* m, const char* filename);
 int      matcher_attach(Matcher* m, const char* filename);
+int      matcher_verify(Matcher* m, const char* filename);
 void     matcher_set_tombstones(Matcher* m, AV* ids);
 void     matcher_set_generation(Matcher* m, UV generation);
 UV       matcher_generation(Matcher* m);
@@ -46,5 +47,6 @@
 AV*  bag_best_for(Bag* b, const char* str, int count);
 int  bag_dump(Bag* b, const char* filename);
 int  bag_load(Bag* b, const char* filename);
+int  bag_verify(Bag* b, const char* filename);
 
 #endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/matcher.cc 
new/Cavil-Matcher-1.01/src/matcher.cc
--- old/Cavil-Matcher-1.00/src/matcher.cc       2026-07-21 21:58:59.381353862 
+0200
+++ new/Cavil-Matcher-1.01/src/matcher.cc       2026-08-04 19:40:44.653155107 
+0200
@@ -64,15 +64,31 @@
   auto mf = std::make_unique<MappedFile>();
   if (!mf->map(path)) return false;
   auto seg = std::make_unique<Segment>();
-  if (!seg->open(mf->data(), mf->size())) return false;
+  // Trusted scan path: skip the whole-payload CRC (verified at publish), keep 
structural validation.
+  if (!seg->open(mf->data(), mf->size(), /*verify_crc=*/false)) return false;
   _maps.push_back(std::move(mf));
   _segments.push_back(std::move(seg));
   return true;
 }
 
+bool Matcher::verify(const std::string& path) const {
+  // Full integrity check (CRC + structure) of a segment file, for an explicit 
fsck. The scan path
+  // (load/attach) trusts the CRC of the immutable published cache; this is 
how it is proven on demand.
+  MappedFile mf;
+  if (!mf.map(path)) return false;
+  Segment seg;
+  return seg.open(mf.data(), mf.size(), /*verify_crc=*/true);
+}
+
 bool Matcher::dump(const std::string& path) {
   std::vector<char> buf = _build.compile(_generation);
 
+  // Publish is the one place integrity is proven: full-verify (CRC + 
structure) the freshly compiled
+  // buffer before it is written, so the scan path can safely trust this 
immutable file without re-CRCing
+  // it on every open. A buffer that fails here is never published.
+  Segment check;
+  if (!check.open(buf.data(), buf.size(), /*verify_crc=*/true)) return false;
+
   // Write to a temp file and rename into place, so a crash/short write/full 
disk mid-dump cannot damage
   // an existing good cache at `path` (and a concurrent reader mmapping the 
old file keeps a valid inode
   // across the swap). The cache is disposable/rebuilt from the DB, so no 
fsync is needed.
@@ -94,7 +110,8 @@
   auto mf = std::make_unique<MappedFile>();
   if (!mf->map(path)) return false;
   auto seg = std::make_unique<Segment>();
-  if (!seg->open(mf->data(), mf->size())) return false;
+  // Trusted scan path: skip the whole-payload CRC (verified at publish), keep 
structural validation.
+  if (!seg->open(mf->data(), mf->size(), /*verify_crc=*/false)) return false;
 
   clear();
   _maps.push_back(std::move(mf));
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/matcher.h 
new/Cavil-Matcher-1.01/src/matcher.h
--- old/Cavil-Matcher-1.00/src/matcher.h        2026-07-21 21:42:39.523424438 
+0200
+++ new/Cavil-Matcher-1.01/src/matcher.h        2026-08-04 19:40:39.133673080 
+0200
@@ -51,6 +51,7 @@
 
   // Segmented surface used by the Perl manifest layer.
   bool attach(const std::string& path);                       // add one 
mmapped segment to the active set
+  bool verify(const std::string& path) const;                 // full 
CRC+structure check of a segment file
   void set_tombstones(const std::vector<uint32_t>& ids);      // pattern ids 
to drop before resolution
   void     set_generation(uint64_t g) { _generation = g; }
   uint64_t generation() const { return _generation; }    // the generation 
this engine was pinned to
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/segment.cc 
new/Cavil-Matcher-1.01/src/segment.cc
--- old/Cavil-Matcher-1.00/src/segment.cc       2026-07-21 21:58:52.108264151 
+0200
+++ new/Cavil-Matcher-1.01/src/segment.cc       2026-08-04 19:38:40.080716593 
+0200
@@ -152,7 +152,7 @@
   return open(_owned.data(), _owned.size());
 }
 
-bool Segment::open(const char* data, size_t len) {
+bool Segment::open(const char* data, size_t len, bool verify_crc) {
   _valid  = false;
   _base   = data;
   _len    = len;
@@ -185,7 +185,11 @@
   total += skip_bytes;
   if (total != len) return false;
 
-  if (cavil_crc32(data + sizeof(SegmentHeader), total - sizeof(SegmentHeader)) 
!= h->payload_crc32) return false;
+  // Corruption detection over the whole payload. Skipped on the trusted scan 
path (see the header doc):
+  // structural validation below still runs, so a corrupt segment is rejected 
or safe regardless.
+  if (verify_crc
+    && cavil_crc32(data + sizeof(SegmentHeader), total - 
sizeof(SegmentHeader)) != h->payload_crc32)
+    return false;
 
   // Semantic header invariants. The CRC only proves the bytes are the ones 
the writer produced; it does
   // not prove a *malicious or buggy* writer produced sane values. These 
fields steer the scan (notably
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/src/segment.h 
new/Cavil-Matcher-1.01/src/segment.h
--- old/Cavil-Matcher-1.00/src/segment.h        2026-07-21 21:58:18.730364439 
+0200
+++ new/Cavil-Matcher-1.01/src/segment.h        2026-08-04 19:38:23.429846009 
+0200
@@ -106,7 +106,15 @@
 
   // Open a segment over an external, read-only buffer (e.g. an mmap). Returns 
false (and leaves the
   // segment unusable) if the buffer is not a valid segment. Never crashes on 
bad input.
-  bool open(const char* data, size_t len);
+  //
+  // verify_crc controls only the whole-payload CRC32 (corruption detection). 
It defaults to true - the
+  // compile/publish path and explicit integrity checks re-checksum 
everything. The scan path passes
+  // false: segments are Cavil's own immutable, atomically-published cache, 
already CRC-verified when
+  // written, so re-CRCing the entire payload on every mmap-open in every 
forked job is wasted work.
+  // The O(1) header/size checks and the full structural validation walk run 
either way, so a bad
+  // segment is always rejected or safe to scan - skipping the CRC never costs 
memory safety, only
+  // per-scan detection of on-disk bit-rot (which publish-time verification 
already covers).
+  bool open(const char* data, size_t len, bool verify_crc = true);
 
   // Take ownership of an in-memory compiled buffer.
   bool open_owned(std::vector<char>&& buf);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/t/09segment.t 
new/Cavil-Matcher-1.01/t/09segment.t
--- old/Cavil-Matcher-1.00/t/09segment.t        2026-07-21 21:18:56.000000000 
+0200
+++ new/Cavil-Matcher-1.01/t/09segment.t        2026-08-04 20:15:56.000000000 
+0200
@@ -85,21 +85,29 @@
 # --- Format safety: corrupt segments are rejected, never mis-read or crashed 
on -------------------
 my $good = slurp($seg_a);
 
-sub attach_bytes {
-  my $bytes = shift;
-  my $f     = "$dir/corrupt.$$." . int(rand(1e9));
+# The scan path (attach/load) always validates a segment's shape and structure 
- that is what keeps a bad
+# file from ever being mis-read or crashed on. It deliberately does NOT 
re-checksum the whole payload:
+# segments are Cavil's own immutable, atomically-published cache, CRC-verified 
when written. The full CRC
+# is available on demand via verify() (and runs at publish time).
+sub attach_bytes { _try_bytes('attach', @_) }
+sub verify_bytes { _try_bytes('verify', @_) }
+
+sub _try_bytes {
+  my ($method, $bytes) = @_;
+  my $f = "$dir/corrupt.$$." . int(rand(1e9));
   open my $fh, '>', $f or die $!;
   binmode $fh;
   print {$fh} $bytes;
   close $fh;
   my $m  = Cavil::Matcher::init_matcher();
-  my $ok = $m->attach($f);
+  my $ok = $m->$method($f);
   unlink $f;
   return $ok;
 }
 
 is(attach_bytes($good), 1, 'a valid segment attaches');
 
+# Structural/shape corruption is always rejected, even on the trusted scan 
path.
 my $bad_magic = $good;
 substr($bad_magic, 0, 1) = 'X';
 is(attach_bytes($bad_magic), 0, 'wrong magic rejected');
@@ -108,15 +116,34 @@
 substr($bad_version, 8, 4) = pack('L', 999);
 is(attach_bytes($bad_version), 0, 'wrong format version rejected');
 
-my $bad_crc = $good;
-substr($bad_crc, length($bad_crc) - 1, 1) = chr((ord(substr($bad_crc, 
length($bad_crc) - 1, 1)) ^ 0xFF));
-is(attach_bytes($bad_crc), 0, 'flipped payload byte fails CRC');
-
 is(attach_bytes(substr($good, 0, 20)),                                0, 
'truncated file rejected');
 is(attach_bytes(''),                                                  0, 
'empty file rejected');
 is(attach_bytes('not a segment at all, just random text bytes here'), 0, 
'garbage file rejected');
 is(attach_bytes($good . 'trailing junk'),                             0, 
'trailing bytes after payload rejected');
 
+# An out-of-range index is caught by the structural walk, which runs on the 
scan path regardless of the
+# CRC - so memory safety never depends on the checksum. Packed layout is 
header(56) | nodes | children |
+# skips; FlatNode is 20 bytes, and each FlatChild starts with an 8-byte hash 
then a 4-byte child_node.
+my ($node_count, $child_count) = unpack('L L', substr($good, 32, 8));
+cmp_ok($child_count, '>', 0, 'segment A has children to corrupt');
+my $bad_index      = $good;
+my $child_node_off = 56 + $node_count * 20 + 8;
+substr($bad_index, $child_node_off, 4) = pack('L', 0xFFFFFFFF);
+is(attach_bytes($bad_index), 0, 'out-of-range child index rejected on the scan 
path (memory safety)');
+
+# The whole-payload CRC is checked by verify(), not by the scan path. Corrupt 
only the stored CRC field
+# (header offset 48) so the payload and structure stay valid: verify() rejects 
it, the scan path trusts it.
+is(verify_bytes($good), 1, 'verify() passes a good segment');
+my $bad_stored_crc = $good;
+substr($bad_stored_crc, 48, 4) = pack('L', unpack('L', substr($good, 48, 4)) ^ 
0xFFFFFFFF);
+is(verify_bytes($bad_stored_crc), 0, 'verify() catches a bad CRC');
+is(attach_bytes($bad_stored_crc), 1, 'scan path trusts a structurally-valid 
segment despite a bad CRC');
+
+# A flipped payload byte is likewise caught by the full check.
+my $bad_crc = $good;
+substr($bad_crc, length($bad_crc) - 1, 1) = chr((ord(substr($bad_crc, 
length($bad_crc) - 1, 1)) ^ 0xFF));
+is(verify_bytes($bad_crc), 0, 'verify() catches a flipped payload byte');
+
 # A matcher whose only segment failed to attach simply finds nothing (no 
crash).
 my $none = Cavil::Matcher::init_matcher();
 $none->attach("$dir/does-not-exist.seg");
@@ -302,12 +329,11 @@
   ok($e->load($good_seg), 'load a good segment');
   cmp_deeply($e->find_matches($probe), [[1, 1, 1]], 'the loaded segment 
matches');
 
-  # A corrupt segment (flipped payload byte -> CRC failure) fails to load...
-  my $bad_bytes = slurp($good_seg);
-  substr($bad_bytes, length($bad_bytes) - 1, 1) = chr(ord(substr($bad_bytes, 
length($bad_bytes) - 1, 1)) ^ 0xFF);
+  # A structurally-invalid segment (truncated) fails to load - the scan path 
rejects malformed structure
+  # even though it trusts the CRC of a well-formed file...
   my $bad_seg = "$dir/loadbad.seg";
   open my $bf, '>:raw', $bad_seg or die $!;
-  print {$bf} $bad_bytes;
+  print {$bf} substr(slurp($good_seg), 0, 20);
   close $bf;
   is($e->load($bad_seg), 0, 'loading a corrupt segment fails');
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/t/12bag.t 
new/Cavil-Matcher-1.01/t/12bag.t
--- old/Cavil-Matcher-1.00/t/12bag.t    2026-07-21 15:29:00.000000000 +0200
+++ new/Cavil-Matcher-1.01/t/12bag.t    2026-08-04 20:31:34.083395197 +0200
@@ -71,8 +71,10 @@
 is_deeply($bag->best_for('permission is hereby granted',  0), [], 
'best_for(count=0) returns empty');
 is_deeply($bag->best_for('permission is hereby granted', -1), [], 
'best_for(negative count) returns empty');
 
-# The cache is a versioned, CRC-checked format: truncated / wrong-magic / 
wrong-version / corrupt
-# files are all rejected, and a rejected load must leave any existing model 
intact (not wipe it).
+# The load path validates structure (the record parse bounds-checks every 
read) but does NOT re-checksum
+# the whole payload: the bag is Cavil's own immutable, atomically-published 
cache. Structural corruption
+# is still rejected on load; whole-payload corruption is caught by verify() 
(and at publish time). A
+# rejected load must leave any existing model intact (not wipe it).
 my $sample = slurp('t/fixtures/licenses/04license.1.txt');
 my $before = $loaded->best_for($sample, 1);
 ok(@$before, 'model matches before a bad load');
@@ -80,17 +82,26 @@
 
 sub write_bytes { my ($p, $b) = @_; open my $o, '>:raw', $p or die $!; print 
{$o} $b; close $o; $p }
 
-# Truncated payload (valid header, partial body): CRC fails.
+# Truncated payload (valid header, partial body): the record parse runs out 
and rejects it.
 is($loaded->load(write_bytes("$dir/truncated", substr($good, 0, 48))), 0, 
'truncated bag rejected');
 
 # Not a bag at all (wrong magic).
 is($loaded->load(write_bytes("$dir/garbage", 'not a bag file, just some bytes 
here at all')),
   0, 'wrong-magic bag rejected');
 
-# Valid magic but a flipped payload byte fails the CRC.
+# The whole-payload CRC is checked by verify(), not by the load path. Corrupt 
only the stored CRC field
+# (header offset 12) so the payload stays valid: verify() rejects it, load() 
trusts it.
+is($loaded->verify($file), 1, 'verify() passes a good bag');
+my $bad_stored_crc = $good;
+substr($bad_stored_crc, 12, 4) = pack('V', unpack('V', substr($good, 12, 4)) ^ 
0xFFFFFFFF);
+is($loaded->verify(write_bytes("$dir/badcrc", $bad_stored_crc)), 0, 'verify() 
catches a bad CRC');
+is($loaded->load(write_bytes("$dir/badcrc", $bad_stored_crc)), 1,
+  'load() trusts a valid-payload bag despite a bad CRC');
+
+# A flipped payload byte is likewise caught by the full check.
 my $corrupt = $good;
 substr($corrupt, length($corrupt) - 1, 1) = chr(ord(substr($corrupt, 
length($corrupt) - 1, 1)) ^ 0xFF);
-is($loaded->load(write_bytes("$dir/corrupt", $corrupt)), 0, 'flipped payload 
byte fails CRC');
+is($loaded->verify(write_bytes("$dir/corrupt", $corrupt)), 0, 'verify() 
catches a flipped payload byte');
 
 # Strictness (like the segment reader): a CRC-valid file with trailing bytes 
after the declared records,
 # or with a pattern's tf_idfs not in the strictly-ascending order compare2 
relies on, must be rejected -
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Cavil-Matcher-1.00/t/15segment_header.t 
new/Cavil-Matcher-1.01/t/15segment_header.t
--- old/Cavil-Matcher-1.00/t/15segment_header.t 2026-07-21 21:05:19.000000000 
+0200
+++ new/Cavil-Matcher-1.01/t/15segment_header.t 2026-08-04 20:16:54.000000000 
+0200
@@ -56,6 +56,13 @@
 is(accepts($good),           1,                                 'the 
untampered segment attaches');
 is(crc32(substr($good, 56)), unpack('V', substr($good, 48, 4)), 'pure-Perl 
CRC32 matches the stored payload CRC');
 
+# Guardrail: the on-disk format version must stay 2. The scan path trusts an 
already-published segment's
+# CRC (it verifies structure but not the whole-payload checksum), which makes 
an in-place production
+# upgrade seamless ONLY as long as the format is unchanged. Bumping this 
without a migration plan would
+# let a new writer publish segments an old reader rejects (and vice versa) - 
so if this fails, the format
+# changed and the upgrade is no longer drop-in.
+is(unpack('V', substr($good, 8, 4)), 2, 'segment format version is still 2 
(in-place upgrade stays seamless)');
+
 my $node_count  = unpack('V', substr($good, 32, 4));
 my $child_count = unpack('V', substr($good, 36, 4));
 my $skip_count  = unpack('V', substr($good, 40, 4));

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.LSAZqV/_old  2026-08-06 16:29:00.522671973 +0200
+++ /var/tmp/diff_new_pack.LSAZqV/_new  2026-08-06 16:29:00.526672113 +0200
@@ -1,6 +1,6 @@
-mtime: 1784733830
-commit: 48884f1377224ebc38ebf8246b04ad87a8bed51bf66dd79fb9ae0f21d88590e9
+mtime: 1785916160
+commit: a3fe64e68ac939b73fcd0b0e07207129b78d41c9f326f32c4073c47d6e5167fc
 url: https://src.opensuse.org/perl/perl-Cavil-Matcher
-revision: 48884f1377224ebc38ebf8246b04ad87a8bed51bf66dd79fb9ae0f21d88590e9
+revision: a3fe64e68ac939b73fcd0b0e07207129b78d41c9f326f32c4073c47d6e5167fc
 projectscmsync: https://src.opensuse.org/perl/_ObsPrj
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-08-05 09:49:20.000000000 +0200
@@ -0,0 +1,5 @@
+*.obscpio
+*.osc
+_build.*
+_service:*
+.pbuild

Reply via email to