fix(rtl,pp): pre-release safety round (Wave 2)

Five concrete gaps the audit flagged in the new __dbCopy / __dbSort /
__dbTotal / __dbJoin / PP code:

  * wam.Close() errors were dropped on the floor. Caller saw `.T.`
    even when the just-written DBF wasn't durable, leading to the
    classic "delete the source after the COPY succeeds" data-loss
    pattern. All four functions now capture the close error and
    return `.F.` if it fired.

  * drv.Create succeeded → wam.Open failed → orphaned-on-disk DBF.
    The user-named target file was left around with zero records,
    and the next call's drv.Create silently truncated it instead of
    surfacing the original error. Add `os.Remove(cFile)` on the
    Open-failure cleanup path for COPY/SORT/TOTAL/JOIN.

  * __dbTotal would write the DBF codec's overflow sentinel
    (`*****`) into the destination's sum-fields when a group total
    didn't fit in the source's declared field width, and still
    return `.T.`. Now: precompute each sum-field's max representable
    magnitude (10^(Len-Dec)) at start, mark the run as overflowed if
    any flush sees an out-of-range or NaN value, and propagate
    `.F.` to the caller so they don't trust the file.

  * cleanUnreferencedMarkers walked byte-by-byte and stripped any
    `<ident>` token in the result, INCLUDING ones that appear
    inside `"..."` / `'...'` string literals. A user expression
    like `LIST FOR url == "<a>x</a>"` got the `<a>` and `</a>`
    eaten on output. Now: track string-literal state and skip the
    cleanup pass while inside one. Bracket-strings `[…]` are
    intentionally not treated as strings here — the result template
    uses `[...]` as the optional-repeat marker, and disambiguating
    needs context the cleanup pass doesn't have.

  * (#8 SET SAFETY honoring) deferred. Harbour default is SAFETY
    OFF, so the current always-overwrite behavior matches default
    Harbour. The divergence only matters when user explicitly does
    `SET SAFETY ON`, which Five doesn't support yet — so the
    no-overwrite-protection is consistent end-to-end. Tracked as a
    separate followup.

Gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 07:54:41 +09:00
parent 000500e034
commit f30704a854
2 changed files with 101 additions and 11 deletions

View File

@@ -1059,30 +1059,54 @@ func referencedMarkers(s string) []string {
return out
}
// cleanUnreferencedMarkers removes any remaining <name>, <(name)>, <.name.>, #<name> references.
// Only removes well-formed PP marker references, not comparison operators.
// cleanUnreferencedMarkers removes any remaining <name>, <(name)>,
// <.name.>, #<name> references. Only removes well-formed PP marker
// references, not comparison operators. Skips over PRG string
// literals ("...", '...', [...]) so a captured value containing
// `<a>` text (e.g. "<a>http://x</a>" inside a regex/string) isn't
// gutted — that pass used to corrupt arbitrary string content.
func cleanUnreferencedMarkers(s string) string {
// Match patterns like <identifier>, <(identifier)>, <.identifier.>, #<identifier>
var out strings.Builder
i := 0
inStr := byte(0)
for i < len(s) {
c := s[i]
// Inside a string literal: copy until the matching closer.
// Bracket-strings `[...]` are PRG-specific but are also used
// as the result template's optional-repeat brackets, so we
// leave them out of this pass — only `'…'` and `"…"` are
// unambiguous strings here.
if inStr != 0 {
out.WriteByte(c)
if c == inStr {
inStr = 0
}
i++
continue
}
if c == '"' || c == '\'' {
inStr = c
out.WriteByte(c)
i++
continue
}
removed := false
// #<name>
if s[i] == '#' && i+1 < len(s) && s[i+1] == '<' {
if c == '#' && i+1 < len(s) && s[i+1] == '<' {
if end := findMarkerEnd(s, i+1); end > 0 {
i = end
removed = true
}
}
// <name>, <(name)>, <.name.>, <"name">
if !removed && s[i] == '<' {
if !removed && c == '<' {
if end := findMarkerEnd(s, i); end > 0 {
i = end
removed = true
}
}
if !removed {
out.WriteByte(s[i])
out.WriteByte(c)
i++
}
}