QA finding: internal/cronoclient/daterange.go::parseDateValue parses the numeric prefix with fmt.Sscanf(s[:len(s)-1], "%d", &n), which accepts:
-
Negative numbers: --since -5d → n = -5 → today.AddDate(0,0,-(-5)) → 5 days in the future. The resulting range fails the inverted-range check with a misleading error rather than rejecting the bad input up front:
$ crono-export nutrition --since -5d
error: --until (2026-05-18) is before --since (2026-05-23)
-
Decimal numbers: --since 1.5d → Sscanf parses up to the ., returns n = 1 with no error. Silently treated as 1d. No warning, no rejection.
Both cases should fail at parseDateValue with a clear message like "N in Nd must be a non-negative integer". The contract documents Nd/Nw/Nm/Ny with N implicitly a non-negative integer — current code is lax about both.
Suggested fix: validate prefix matches ^\d+$ before parsing, or after Sscanf check that (a) the number is >= 0 and (b) the consumed length equals len(s)-1. Add unit tests for -5d, 1.5d, 5.0d, +5d, 5d, 5 d.
Severity: minor (no data corruption, but "works" with surprising semantics)
QA finding:
internal/cronoclient/daterange.go::parseDateValueparses the numeric prefix withfmt.Sscanf(s[:len(s)-1], "%d", &n), which accepts:Negative numbers:
--since -5d→ n = -5 → today.AddDate(0,0,-(-5)) → 5 days in the future. The resulting range fails the inverted-range check with a misleading error rather than rejecting the bad input up front:Decimal numbers:
--since 1.5d→ Sscanf parses up to the., returns n = 1 with no error. Silently treated as1d. No warning, no rejection.Both cases should fail at parseDateValue with a clear message like "N in Nd must be a non-negative integer". The contract documents Nd/Nw/Nm/Ny with N implicitly a non-negative integer — current code is lax about both.
Suggested fix: validate prefix matches
^\d+$before parsing, or after Sscanf check that (a) the number is >= 0 and (b) the consumed length equals len(s)-1. Add unit tests for-5d,1.5d,5.0d,+5d,5d,5 d.Severity: minor (no data corruption, but "works" with surprising semantics)