Cleanup: Replace sscanf with strtol/strtoul in snapmgr
Context and Motivation
This thread concerns a focused refactoring patch targeting ImportSnapshot and its helper parsers (parseIntFromText, parseXidFromText, parseVxidFromText) in src/backend/utils/time/snapmgr.c. These helpers parse the text-format snapshot files written by pg_export_snapshot() and read by SET TRANSACTION SNAPSHOT. Each line of such a file has a simple key:value structure containing xids, xid lists, and a virtual transaction id (vxid) formatted as procno/lxid.
The existing implementation uses sscanf() for each field. While correct, sscanf() has two well-known drawbacks in this kind of line-at-a-time parser:
- No cheap way to advance past what was consumed.
sscanfdoes not by default report how many characters it consumed (absent the%nconversion), so the calling code has to separately scan forward for the next newline to locate the next record. This amounts to re-scanning bytes that were already examined by the parser. - Weaker error semantics.
sscanfconflates "no conversion" and "partial conversion"; detecting overflow requires checkingerrnoin a way that is not uniformly portable, and the integer conversion specifiers (%d,%u,%lu) silently accept leading whitespace and do not distinguish between "nothing there" and "garbage after the number".
The patch converts these helpers to use strtol/strtoul, relying on the endptr out-parameter so the caller can both (a) validate that at least one digit was consumed and (b) resume parsing from the returned pointer without an additional linear scan for the next delimiter.
Why This Matters Architecturally
Snapshot import/export is on the path of parallel query (workers import the leader's snapshot via pg_export_snapshot machinery internally) and of user-driven repeatable-read coordination across sessions. It's not a hot path in the sense of per-tuple execution, but it is security-sensitive: the snapshot file is read from pg_snapshots/ and its contents directly influence visibility. Adopting the idiomatic, end-pointer-driven parsing style used elsewhere in the backend (e.g., GUC parsing, pg_lsn_in, array input functions) reduces the cognitive surface area for auditing this code and removes a small, avoidable O(n) rescan per field.
The patch is therefore explicitly framed as cleanup/alignment rather than a bug fix — the submitter, Amul Sul, positions it as (1) efficiency via endptr reuse and (2) consistency with the rest of the backend's parsing conventions.
Design Discussion
Preservation of existing leniency
Andreas Karlsson (reviewer) explicitly flagged a subtle semantic point: the old sscanf-based code silently ignored trailing garbage on each line, and the new strtol-based code preserves that behavior. He characterized this as "for good and bad" — it is lenient input handling which could mask corrupt or tampered snapshot files, but tightening it would be a behavior change outside the scope of a cleanup patch. The consensus was to keep behavior bit-identical.
Whitespace handling equivalence
Tristan Partin verified an easy-to-miss equivalence: sscanf(" 45", "%d", ...) skips leading whitespace, and strtol/strtoul also skip leading whitespace by definition (per C99 7.20.1.4). So switching parsers does not introduce a regression for lines that happen to have leading spaces. This is the kind of check that typically gets missed in "obvious" parser swaps, and explicitly confirming it in-thread is what moved the patch toward committable status.
Variable naming bikeshed (with substance)
Tristan suggested renaming the locally-declared integers in parseVxidFromText from short forms to procNumber and localTransactionId to match the domain vocabulary used elsewhere (recall that BackendId was renamed to ProcNumber in the PG17 cycle, and VirtualTransactionId contains backendId/localTransactionId fields — now procNumber/localTransactionId). Amul pushed back mildly, preferring procno and xid for brevity, and extended the pattern to parseXidFromText (renaming val → xid) while keeping val in the generic parseIntFromText. This is a defensible compromise: procno matches the procNumber field's conventional abbreviation used in many comments, and keeping val for the generic helper signals that it is field-type-agnostic.
What the Patch Actually Changes
At the level of each helper:
parseIntFromText: Replaces ansscanf(s, "%u", &val)pattern plus a manualstrchr(s, '\n')advance with astrtoul(s, &endptr, 10)call, whereendptrboth validates conversion (by comparing to the input pointer) and provides the resumption point.parseXidFromText: Same transformation usingstrtoul(xids are 32-bit unsigned). Local variable renamedval→xid.parseVxidFromText: Parses two integers separated by/. Now callsstrtol/strtoultwice, checking that the intermediateendptrpoints at/and that the finalendptris at end-of-field. Locals renamed toprocnoandxid(forlocalTransactionId).
The net effect is shorter code, one fewer pass over each line, and parsing that participates in the usual endptr/errno error-reporting idiom already used across the backend.
Review Trajectory
The patch is small, isolated, and non-behavioral. It drew two reviewers — Andreas Karlsson (general +1 after manual review, noting the preserved leniency) and Tristan Partin (naming suggestion, whitespace-equivalence verification). After the naming revision in v2, Tristan confirmed the diff was limited to the rename and gave a second LGTM. The thread reads as ready-for-commit; no committer has yet applied it within the captured messages, but there are no open objections.
Risks and Residual Concerns
- Overflow detection:
strtoulreturnsULONG_MAXon overflow and setserrno = ERANGE. On 64-bit platforms this is a wider range than the 32-bit xid/procno fields, so the helpers should (and presumably do, though not shown in-thread) additionally range-check againstUINT32_MAXor similar. Neither reviewer raised this explicitly, which is a minor gap in the review. - Locale sensitivity:
strtolrespectsLC_NUMERIConly in its hex/octal prefix handling behavior in some libcs; base-10 parsing is locale-independent in practice. This matchessscanf's base-10 behavior, so no regression. - Leniency preservation: As noted, silently ignoring trailing garbage is preserved; hardening is left for a potential future patch.