feat(13-01): add the prohibited rule and dated-download and notification masks

- lagoon.ValidateRequest supports Laravel 9 prohibited (!required, not
  implicit); with no catalog line its message is validation.prohibited
- tide compares Content-Disposition with real calendar dates masked on both
  sides; a different name, an invalid date or a one-sided date still diffs
- tide.NormalizePublications masks a Carbon +00:00 $.data.payload.created_at
  and an uncaptured positive integer $.data.payload.id as {{id}}
- the album-date test's outside-album sibling moves off payload.created_at,
  which now has its own mask
- READMEs and docs describe the rule and both masks
This commit is contained in:
Jakub Zych
2026-10-03 06:32:46 +02:00
parent 55a4092019
commit 2b94dfd2d2
10 changed files with 243 additions and 9 deletions

View File

@@ -6,8 +6,10 @@ import (
"fmt"
"mime"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
)
@@ -81,6 +83,9 @@ func compareHeaders(want, got, extra map[string]string) []Diff {
if gv == wv {
continue
}
if ck == "Content-Disposition" && sameDispositionButDates(wv, gv) {
continue
}
if gv == "" {
gv = "<missing>"
}
@@ -290,3 +295,32 @@ func quotePrintable(b []byte) string {
buf.WriteByte('"')
return buf.String()
}
// dispositionDateRe finds YYYY-MM-DD tokens in a Content-Disposition value,
// such as the date in a download name built from the current day.
var dispositionDateRe = regexp.MustCompile(`\b\d{4}-\d{2}-\d{2}\b`)
// maskDispositionDates replaces every date token with one placeholder and
// reports how many it replaced. ok is false when a token is not a real
// calendar date, so the caller falls back to the byte comparison.
func maskDispositionDates(s string) (masked string, n int, ok bool) {
ok = true
masked = dispositionDateRe.ReplaceAllStringFunc(s, func(m string) string {
if _, err := time.Parse("2006-01-02", m); err != nil {
ok = false
return m
}
n++
return "{{date}}"
})
return masked, n, ok
}
// sameDispositionButDates reports whether two Content-Disposition values
// differ only in their dates: both carry the same number of real calendar
// dates and are equal once those are masked. Anything else stays a Diff.
func sameDispositionButDates(want, got string) bool {
wm, wn, wok := maskDispositionDates(want)
gm, gn, gok := maskDispositionDates(got)
return wok && gok && wn > 0 && wn == gn && wm == gm
}