Revision history for Genealogy::Wills

0.11	Tue Aug 11 07:40:15 PM EDT 2026
	[ Enhancements ]
	- Add t/path.t: 35-subtest exhaustive CFG path-coverage suite (LCSAJ/TER3).
	  Maps all unique execution paths through new() (18 paths: PN-1 to PN-18),
	  search() (12+ paths: PS-1 to PS-12), and _decorate_will() (1 sequential
	  path: PD-1). Covers: ::new() function form with undef class (//= fires),
	  clone paths (single-string, no-args // {} guard, flat-list override),
	  config_file readable vs unreadable (D=TRUE/FALSE), all three logger-croak
	  branches (not blessed, missing info(), missing error()), logger=undef
	  defined() skip, directory //= MODULE_DATA_DIR branch, -d+r check both
	  directions, unreadable directory (chmod 0, SKIP for root/Win32), A-branch
	  (single-string arg) vs get_params branch for class->new(), PVS throws on
	  bad last/optional-field/year (5 variants), D=TRUE carp in list+scalar
	  context, wills->new()=undef croak, list context 0-iter and N-iter loops
	  (fixate count proxy), scalar fetchrow=undef bare return, scalar
	  fetchrow=defined Return::Set path, ||= short-circuit on pre-set slot,
	  bare-string and hashref arg normalization.  Uses inline _PT_MockDB_Rows/
	  _PT_MockDB_Empty packages (real named methods) to avoid Test::Mockingbird
	  AUTOLOAD-cache decay that causes fetchrow_hashref to return 1 after
	  multiple restore_all() cycles.  Dead-code summary: none.  Loop analysis:
	  for-loop in search() executes 0 or N>=2 times (no annotation required).
	- Add t/extended_tests.t: 14-subtest coverage-gap suite targeting untested
	  LCSAJ/TER3 paths. Covers: scalar context + undef last (carps and returns
	  undef, not empty list); new() logger => undef skips the defined() guard
	  branch; wills->new() arg capture (no_entry, no_fixate, directory all
	  forwarded); void context search() (wantarray false, scalar branch taken);
	  hashref argument form happy path; Data::Reuse::fixate call count equals
	  row count in list context and is zero for empty list; Return::Set::set_return
	  invoked exactly once in scalar non-null path; alternating list/scalar calls
	  on same object (no cross-contamination); clone inheriting pre-set wills slot
	  (wills::new() not called again on clone); first/middle/town/year optional
	  fields forwarded verbatim to DB layer (apostrophe, period, hyphen, comma
	  preserved); bare-string form maps to { last => string } verified at DB
	  boundary; clone with no-arg new() inherits all original attribute values.
	- Regex audit: lib/Genealogy/Wills.pm: $MODULE_DATA_DIR s/\.pm$// → s/\.pm\z// (strict
	  end-of-string; $ matches before trailing \n, \z does not)
	- Regex audit: lib/Genealogy/Wills.pm: $SEARCH_SCHEMA matches qr/^[\w\-]+$/ → qr/^[\w-]+\z/a:
	  \z for strict eos; idiomatic unescaped [-] at end of character class; /a restricts \w
	  to ASCII [0-9A-Za-z_] blocking Unicode homograph queries (Finding 2 applied)
	- Regex audit: bin/create_db.PL: replace $line =~ /Created by/ with index() for O(1)
	  literal substring check (avoids regex engine startup cost per line)
	- Regex audit: bin/create_db.PL: rewrite main HTML parsing regex with /x modifier and
	  negated character classes: [^"]+ for href (URL cannot contain "), [^,<>]+ for last
	  name (bounded by comma delimiter), [^<]+? non-greedy for first name (terminates at
	  </a>), .+? non-greedy for town (year anchor stops correctly) — eliminates ReDoS
	  from greedy (.+) groups that could backtrack catastrophically on malformed input
	- Regex audit: bin/create_db.PL: Alias regex /(.+)\sAlias\s(.+)/ → /\A(.+?)\s+Alias\s+(.+)\z/x:
	  anchored; non-greedy first group splits on FIRST Alias; \s+ tolerates multi-space
	- Regex audit: bin/create_db.PL: slash alias regex /(.+)\/(.+)/ → /\A([^\/]+)\/(.+)\z/x:
	  [^/]+ for first part (no backtracking possible; alias cannot contain /); anchored
	- Regex audit: bin/create_db.PL: town "next" regex /(.*) next (.*?)/i → /x multiline
	  with .+ (not .*) requiring non-empty place names; \s+ around "next" for robustness
	- Regex audit: bin/create_db.PL: first/middle split /^(.+?)\s(.+)$/ → /^(\S+)\s+(.+)\z/:
	  \S+ (non-whitespace) for first-name token avoids backtracking; \s+ handles double-
	  spaced input; \z is strictly end-of-string
	- Bump minimum versions
	- Added the test dashboard
	- Modernize Genealogy::Wills.pm: use autodie, explicit Scalar::Util import, simplified
	  directory fallback, optimised list-context return in search()
	- Modernize bin/create_db.PL: remove old-style sub prototypes, replace SQL
	  string-interpolation inserts with DBI prepared statements, state cache in normalise_name
	- Add =head1 LIMITATIONS POD section documenting ::new() behaviour, Sub::Private gap,
	  load-time year cap, and single-source data limitation
	- Add =head1 SECURITY POD section documenting attack surface, mitigations, and
	  known findings for first/middle/town (no matches constraint), Unicode \w without /a,
	  and Object::Configure logger replacement behaviour
	- Enhance POD for new() and search() with EXAMPLE, API SPECIFICATION, MESSAGES,
	  FORMAL SPECIFICATION, and PSEUDOCODE sub-sections
	- Makefile.PL: remove unused File::Slurp, File::Basename, File::pfopen prerequisites;
	  raise MIN_PERL_VERSION from 5.6.2 to 5.010 (required by use feature 'state');
	  add Readonly to PREREQ_PM
	- bin/create_db.PL: use Carp::croak instead of die; localize $| with local $| = 1
	  to prevent output-buffering state leaking across calls; fix misleading memory comment
	- Add t/locales.t: POSIX locale robustness tests verifying hardcoded error messages
	  are locale-independent; uses $! directly (not POSIX::strerror) to avoid C-library
	  divergence
	- Add t/cgi_security.t: 20-subtest penetration test suite covering SQL injection,
	  XSS, shell injection, CRLF, null bytes, path traversal, type confusion, DoS/ReDoS,
	  hostile logger injection, CGI QUERY_STRING and POST body simulation; no real DB needed
	- Performance: replace Module::Info->new_from_loaded(__PACKAGE__)->file() in new()
	  with a compile-time __FILE__ constant ($MODULE_DATA_DIR) computed once at module
	  load; eliminates a per-call %INC scan and Module::Info object allocation; removes
	  Module::Info dependency entirely from module and Makefile.PL
	- Performance: hoist the Params::Validate::Strict schema out of search() into a
	  module-level constant ($SEARCH_SCHEMA); eliminates 6 transient hashref allocations
	  per search() call -- meaningful at high call rates (e.g. 1000 searches/sec)
	- Performance: bin/create_db.PL: use $dbh->prepare_cached() in flush() instead of
	  prepare() so the INSERT statement plan is reused across batch flushes
	- Performance: bin/create_db.PL: replace shift(@lines)-while with for-loop over
	  split(/\n/,$data); simpler and avoids the slower /$/ms multi-line anchor split
	- Performance: bin/create_db.PL: remove unused 'use HTML::Entities' (never called;
	  LWP's decoded_content() handles entity decoding; saves ~3ms at startup)
	- Logic-reducer: replace use constant with Readonly my $... for all four module
	  constants (DEFAULT_CACHE_DURATION, MIN/MAX_LAST_NAME_LENGTH, MAX_WILL_YEAR);
	  remove dead branches in new() (unreachable ::new()-with-args carp, dead clone guard);
	  remove redundant s/[^\w\-]//g sanitization in search() (transitive reduction -- PVS
	  already enforces the character set); normalize selectall_hashref return to // []
	  eliminating one conditional branch; use cached stat (-r _) after -d in new()
	- t/30-basics.t: add six equivalence-partition boundary tests for year field
	  (year=0 dies, year=1 lives, year=MAX lives, year=MAX+1 dies) and scalar-context
	  hashref return
	- lib/Genealogy/Wills.pm: extract %_FIELD_BASE (shared optional-string schema base)
	  and $_OPT_NAME_RE/$_OPT_TOWN_RE (shared matches regexes) to eliminate three copies
	  of { type => 'string', optional => 1, min => 1, max => 100 } and two copies of
	  qr/^[\w '.-]+\z/ in $SEARCH_SCHEMA
	- lib/Genealogy/Wills.pm: extract _decorate_will() private helper to eliminate
	  duplicated URL-prefix injection + Data::Reuse::fixate logic in search() list and
	  scalar branches; update LIMITATIONS POD to document the helper and why Sub::Private
	  is not used (CHECK block timing conflict with test harness)
	- bin/create_db.PL: refactor queue() alias/slash if/elsif: both branches had an
	  identical two-line body (push @queue + $last=$2); now captures ($alt, $primary)
	  in the pattern-match phase and executes the shared action once
	- Add t/edge_cases.t: 35-assertion destructive/boundary/security test suite.
	  Covers: last field \z-anchor regression (trailing \n proof), null byte
	  injection, Unicode Cyrillic homograph attack (proves /a modifier), 1/100/101
	  char boundaries, type confusion (arrayref/hashref/coderef for last), empty-
	  string and above-max for first/middle/town, XSS payloads (< > blocked),
	  CRLF and null-byte injection in optional fields, shell metacharacters, year
	  boundary (0/1/-1/float/non-numeric), DB upstream failures (undef/0/die on
	  selectall_hashref, die on fetchrow_hashref, new() returns undef), row
	  corruption (missing url key produces bare 'https://', pre-schemed url gets
	  doubled), $@ preservation regression (PVS clobber fix), $_ preservation
	  during list-context and croak-path search(), wrong-class blessed invocant,
	  unblessed-hashref invocant, /dev/null and file-as-directory in new(),
	  unreadable config file (non-root SKIP), config file is a directory, alarm()
	  not consumed by search(), 1000-row stress test (all rows decorated), duplicate
	  keys last-wins, first=>undef optional field. Documents fragility: // [] guard
	  only replaces undef; selectall_hashref returning 0 exposes this assumption.
	- Add t/integration.t: 23-subtest end-to-end integration suite. Covers: complete
	  new()->search() workflows in list and scalar context; bare-string, hashref, and
	  named-arg argument forms; config-file-driven construction (YAML); ENV override
	  (Genealogy__Wills__directory) taking precedence over config; clone (instance->new())
	  lifecycle; two independent objects not sharing wills DB slots; cross-module
	  interaction verification via spy on Data::Reuse::fixate and call-counting mock on
	  Genealogy::Wills::wills::new() (confirms lazy-init fires exactly once per object);
	  capturing mock on selectall_hashref confirming all five search fields forwarded
	  verbatim; url-decoration invariant (https:// prepended exactly once in both list
	  and scalar contexts); all documented error paths (bad directory, bad config_file,
	  bad logger, DB-init failure, class-method call, no-argument call); note documenting
	  absence of optional runtime dependencies
	[ Bug Fixes ]
	- lib/Genealogy/Wills.pm: fix $@ clobber bug: Params::Validate::Strict uses
	  eval internally and resets $@ to '' on successful validation. Added
	  local $@ in a do-block around the validate_strict() call in search() so
	  the caller's $@ is preserved. Revealed by t/unit.t global-state integrity test.
	- lib/Genealogy/Wills.pm: fix dead-code bug where the logger validation check
	  (blessed && can 'info' && can 'error') ran AFTER Object::Configure::configure(),
	  which always replaces the caller's logger before the check ran. Moved validation
	  BEFORE configure() so bad-interface loggers cause an immediate croak. Revealed
	  by t/function.t which tests intended rather than actual behaviour. Updated
	  t/cgi_security.t section 15 to assert the corrected behaviour: bad loggers
	  croak early; valid-interface trojans still get replaced by Object::Configure.
	- Work around MakeMaker copying to blib *before* running the script to build the files to go there
	- search() now croaks with "Usage: ..." when called with no arguments
	- new() now croaks with "Can't load configuration from ..." when the specified
	  config_file does not exist or is not readable
	- t/wills.t: HTTP URL checks now skip gracefully when the upstream site returns non-200
	- use Carp () — prevent default Carp exports so all carp/croak calls stay
	  fully-qualified and remain interceptable by Test::Carp at runtime; bare imported
	  aliases (compile-time copies) bypassed the runtime override
	- new() directory-not-found carp was a bare carp() call; changed to Carp::carp()
	- search() sanitization regex s/[^\w\-']//g allowed apostrophes that the validation
	  pattern qr/^[\w\-]+$/ rejects; fixed to s/[^\w\-]//g; subsequently removed
	  entirely as a transitive reduction (PVS enforces the constraint upstream)
	- bin/create_db.PL: removed dead die check after DBI->connect with RaiseError=>1
	  (DBI throws before returning undef); removed duplicate mkdir block
	- t/carp.t: added test for new() directory-not-found carp path; restructured to
	  avoid done_testing() inside SKIP conflicting with the outer test plan
	- Security audit S1 (MEDIUM, Finding 1 applied): add matches constraints to
	  first/middle/town in $SEARCH_SCHEMA. first/middle: qr/^[\w '.-]+\z/ allows
	  Unicode word chars, space, apostrophe, period, hyphen; blocks ;=|&<>\r\n\0.
	  town: qr/^[\w ',.-]+\z/ additionally allows comma for "Town, County, Country"
	  format. Primary defence remains parameterised queries (Database::Abstraction);
	  matches constraints add defence-in-depth. Residual: Smith'-- passes (valid
	  name chars) but is neutralised by parameterised queries.
	- Security audit S1: update t/cgi_security.t section 10 from lives_ok
	  (documenting pass-through) to dies_ok (asserting rejection at PVS level);
	  remove is($captured{...}) assertions; add three positive tests confirming
	  O'Brien, Canterbury, St. John still pass the new constraints.
	- Security audit S2 (MEDIUM): sanitize $ENV{CACHE_DIR}/$ENV{CACHEDIR} in
	  bin/create_db.PL before use in mkdir and File::Spec::catfile; validate with
	  qr/\A[\w.\-\/~]+\z/ and index(..) < 0 (no .. traversal sequences); croak on
	  invalid value.
	- Security audit S3 (LOW): bin/create_db.PL: replace die on HTTP error with
	  Carp::croak() for consistency with the documented croak/carp convention.
	- Security audit S4 (MEDIUM/INFO): document Finding 4 in SECURITY POD:
	  Object::Configure reads Genealogy__Wills__directory from %ENV before new()
	  applies defaults, allowing a compromised process environment to redirect
	  search() to an attacker-controlled SQLite file; note conditions and
	  mitigations. No code change possible (design decision in Object::Configure).

0.10	Sat Aug 23 20:34:26 EDT 2025
	Fix return in scalar context
	Validate last name
	Fixate arrays

0.09	Wed Aug  6 08:29:26 EDT 2025
	Bump minimum version of Params::Get
	Use Test::Returns

0.08	Thu Jul 31 07:34:52 EDT 2025
	Fix http://www.cpantesters.org/cpan/report/b5e051e4-1e37-11f0-98b0-b3c3213a625c
	Use Object::Configure, Params::Get, Return::Set
	Use YAML::Any instead of YAML::XS

0.07	Wed Apr 16 07:47:03 EDT 2025
	Use Test::HTTPStatus
	Use Data::Reuse

0.06	Fri Jan 10 16:10:24 EST 2025
	search() can now just take one parameter which is the last name
	More handling of broken smokers
		http://cpantesters.org/report/0c13a6c2-4618-11ef-958e-8f2e6e8775ea
	Added t/30-basics.t

0.05	Wed Jul  3 07:27:20 EDT 2024
	Use CACHE_DIR instead of CACHEDIR
	Handle broken smokers
		https://www.cpantesters.org/cpan/report/8a8d8920-f24c-11ee-b45f-18f16d8775ea
		https://www.cpantesters.org/cpan/report/ef9905ca-3a1c-11ef-a8e6-11166e8775ea

0.04	Sun Feb 18 22:25:31 EST 2024
	Added first, last, year as an index
	Use Database::Abstraction

0.03	Sun Oct 15 16:48:19 EDT 2023
	Latest DB.pm from NJH-Snippets

0.02	Fri Mar  3 21:21:02 EST 2023
	Calling new on an object now returns a clone rather than setting the defaults in the new object
	Handle "Ash next Sandwich" by adding both Ash and Sandwich as well
	Avoid adding duplicates into the database
	Fixed URL that's added

0.01	Tue Jan 24 21:00:44 EST 2023
	First draft
