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