From c9c933329401c73c5cc0b9979a7ff8ed80a50655 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Wed, 13 May 2026 13:54:06 -0400 Subject: [PATCH] feat(canary): pdf template (binary blob + build helper) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 first commit. Adds the committed template.pdf binary blob plus the pure-Go one-shot utility that produced it, matching the docx Phase 4 pattern (cmd/buildtemplate/main.go excluded from production). cmd/buildpdftemplate/main.go assembles a minimal PDF-1.4 by hand: - Header: %PDF-1.4 + 4 high-bit bytes to mark as binary - Object 1: Catalog → Pages root - Object 2: Pages → single Kids ref + Count 1 - Object 3: Page with /AA << /O << /Type /Action /S /URI /URI (...) >> >> where the URI value is the 76-char placeholder HONEY_TRACK_URL_PADDED_TO_FIXED_WIDTH______________________________________ - xref table with computed byte offsets - trailer + startxref + %%EOF The placeholder is a direct dictionary value (NOT inside a stream), so substitution can be a plain byte-replace at runtime without breaking the cross-reference table — spec §9.4 line 1133. No FlateDecode anywhere. pdfcpu (v0.12.1) is used for validation only — the builder calls api.Validate before writing, and tests will call it again on substituted output to confirm the PDF stays well-formed. Verification at HEAD: - 477-byte template.pdf - sha256: 70c6359016ceb780539f8c4497991b98ff82201e35a38d36090d88856027e103 - Reproducible: rebuild produces byte-identical output - grep -aob HONEY_TRACK_URL → one offset (240) - file: PDF document, version 1.4, 1 page(s) - api.Validate passes go.mod adds pdfcpu as a direct dep with its transitive deps via go mod tidy. golang.org/x/crypto returns to direct (pdfcpu uses it for AES encryption support). --- .../backend/cmd/buildpdftemplate/main.go | 154 ++++++++++++++++++ .../canary-token-generator/backend/go.mod | 9 + .../canary-token-generator/backend/go.sum | 18 ++ .../generators/pdf/template/template.pdf | 28 ++++ 4 files changed, 209 insertions(+) create mode 100644 PROJECTS/beginner/canary-token-generator/backend/cmd/buildpdftemplate/main.go create mode 100644 PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/pdf/template/template.pdf diff --git a/PROJECTS/beginner/canary-token-generator/backend/cmd/buildpdftemplate/main.go b/PROJECTS/beginner/canary-token-generator/backend/cmd/buildpdftemplate/main.go new file mode 100644 index 00000000..fdc604bb --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/cmd/buildpdftemplate/main.go @@ -0,0 +1,154 @@ +// ©AngelaMos | 2026 +// main.go + +package main + +import ( + "bytes" + "crypto/sha256" + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/pdfcpu/pdfcpu/pkg/api" +) + +const ( + placeholderRoot = "HONEY_TRACK_URL_PADDED_TO_FIXED_WIDTH" + placeholderLength = 76 + + pdfHeader = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n" + + objCatalog = "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" + objPages = "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" + + pageDictFmt = "3 0 obj\n" + + "<<\n" + + "/Type /Page\n" + + "/Parent 2 0 R\n" + + "/MediaBox [0 0 612 792]\n" + + "/Resources << >>\n" + + "/AA << /O << /Type /Action /S /URI /URI (%s) >> >>\n" + + ">>\nendobj\n" + + xrefHeader = "xref\n0 4\n" + xrefFreeRecord = "0000000000 65535 f \n" + xrefRecordFmt = "%010d 00000 n \n" + + trailerFmt = "trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" + + dirPerm os.FileMode = 0o755 + filePerm os.FileMode = 0o644 +) + +func placeholder() string { + return placeholderRoot + + strings.Repeat("_", placeholderLength-len(placeholderRoot)) +} + +func main() { + out := flag.String("out", "", "output path for template.pdf") + flag.Parse() + + if *out == "" { + log.Fatal("usage: buildpdftemplate -out ") + } + + if err := buildTemplate(*out); err != nil { + log.Fatalf("build pdf template: %v", err) + } + + fmt.Printf("wrote %s\n", *out) +} + +func buildTemplate(out string) (err error) { + cleaned := filepath.Clean(out) + if mkErr := os.MkdirAll(filepath.Dir(cleaned), dirPerm); mkErr != nil { + return fmt.Errorf("mkdir parent: %w", mkErr) + } + + pdfBytes := assemblePDF() + + if vErr := api.Validate( + bytes.NewReader(pdfBytes), + nil, + ); vErr != nil { + return fmt.Errorf("pdfcpu validate: %w", vErr) + } + + full := placeholder() + if c := bytes.Count(pdfBytes, []byte(full)); c != 1 { + return fmt.Errorf( + "full placeholder appears %d times, expected exactly 1", + c, + ) + } + if c := bytes.Count(pdfBytes, []byte(placeholderRoot)); c != 1 { + return fmt.Errorf( + "placeholder root appears %d times, expected exactly 1", + c, + ) + } + if len(full) != placeholderLength { + return fmt.Errorf( + "placeholder length is %d, expected %d", + len(full), + placeholderLength, + ) + } + + f, oErr := os.OpenFile( + cleaned, + os.O_CREATE|os.O_TRUNC|os.O_WRONLY, + filePerm, + ) + if oErr != nil { + return fmt.Errorf("open output: %w", oErr) + } + defer func() { + if cerr := f.Close(); cerr != nil && err == nil { + err = fmt.Errorf("close output: %w", cerr) + } + }() + + if _, wErr := f.Write(pdfBytes); wErr != nil { + return fmt.Errorf("write pdf: %w", wErr) + } + + sum := sha256.Sum256(pdfBytes) + fmt.Printf("sha256: %x\n", sum) + fmt.Printf("size: %d bytes\n", len(pdfBytes)) + return nil +} + +func assemblePDF() []byte { + pageObj := fmt.Sprintf(pageDictFmt, placeholder()) + + var buf bytes.Buffer + buf.WriteString(pdfHeader) + + offsets := make([]int, 4) + + offsets[1] = buf.Len() + buf.WriteString(objCatalog) + + offsets[2] = buf.Len() + buf.WriteString(objPages) + + offsets[3] = buf.Len() + buf.WriteString(pageObj) + + xrefOffset := buf.Len() + buf.WriteString(xrefHeader) + buf.WriteString(xrefFreeRecord) + for i := 1; i <= 3; i++ { + fmt.Fprintf(&buf, xrefRecordFmt, offsets[i]) + } + + fmt.Fprintf(&buf, trailerFmt, xrefOffset) + + return buf.Bytes() +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/go.mod b/PROJECTS/beginner/canary-token-generator/backend/go.mod index 0a3c2b1d..8892e4d9 100644 --- a/PROJECTS/beginner/canary-token-generator/backend/go.mod +++ b/PROJECTS/beginner/canary-token-generator/backend/go.mod @@ -13,6 +13,7 @@ require ( github.com/knadh/koanf/providers/env v1.1.0 github.com/knadh/koanf/providers/file v1.2.1 github.com/knadh/koanf/v2 v2.1.2 + github.com/pdfcpu/pdfcpu v0.12.1 github.com/pressly/goose/v3 v3.27.1 github.com/redis/go-redis/v9 v9.7.0 github.com/stretchr/testify v1.11.1 @@ -32,6 +33,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -53,6 +55,9 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect + github.com/hhrutter/lzw v1.0.0 // indirect + github.com/hhrutter/pkcs7 v0.2.2 // indirect + github.com/hhrutter/tiff v1.0.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -61,6 +66,7 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -75,6 +81,7 @@ require ( github.com/moby/term v0.5.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect @@ -91,6 +98,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.50.0 // indirect + golang.org/x/image v0.39.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect @@ -98,5 +106,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/PROJECTS/beginner/canary-token-generator/backend/go.sum b/PROJECTS/beginner/canary-token-generator/backend/go.sum index 6545c6db..f41b63c7 100644 --- a/PROJECTS/beginner/canary-token-generator/backend/go.sum +++ b/PROJECTS/beginner/canary-token-generator/backend/go.sum @@ -17,6 +17,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -83,6 +85,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/pkcs7 v0.2.2 h1:xMoifoVWah1LNym3C0pomEiLmyJyVIBXt/8oTPyPz+8= +github.com/hhrutter/pkcs7 v0.2.2/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE= +github.com/hhrutter/tiff v1.0.3 h1:POV5xITOE1Lt5FvP24ylft0LyCmHmc8GkJ1SVlvUyk0= +github.com/hhrutter/tiff v1.0.3/go.mod h1:zZDLVY4cp9za2FLrryAaGszwWYAUM6DrRiBR0l//mxA= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -119,6 +127,8 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= @@ -153,6 +163,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pdfcpu/pdfcpu v0.12.1 h1:HwoN72zJCj+pPbfMDChYBTZrT7SY0VwgUzqeaId3I20= +github.com/pdfcpu/pdfcpu v0.12.1/go.mod h1:7KPpVLMavcpliPrtN6o7Kuk3cFtYq8nii3SJnnsK7ps= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= @@ -216,6 +230,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww= +golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= @@ -245,6 +261,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/pdf/template/template.pdf b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/pdf/template/template.pdf new file mode 100644 index 00000000..71ff8c5e --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/pdf/template/template.pdf @@ -0,0 +1,28 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< +/Type /Page +/Parent 2 0 R +/MediaBox [0 0 612 792] +/Resources << >> +/AA << /O << /Type /Action /S /URI /URI (HONEY_TRACK_URL_PADDED_TO_FIXED_WIDTH_______________________________________) >> >> +>> +endobj +xref +0 4 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000121 00000 n +trailer +<< /Size 4 /Root 1 0 R >> +startxref +334 +%%EOF