Skip to content

An Operation says which bytes of the stream it is - #33

Merged
tannevaled merged 1 commit into
mainfrom
where-an-operation-was
Oct 11, 2026
Merged

tannevaled merged 1 commit into
mainfrom
where-an-operation-was

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Operation gains Start and End: the byte range of the operation in the
stream it was scanned from — Start at its first operand (or at the operator
when it has none), End just past the operator.

⛔ They are here so a content stream can be edited in place

A tool that wants to put something before every BT has two honest ways to
find one:

  • re-serialise the whole stream, which risks changing what an inline
    image's bytes mean;
  • know where the operator was — and this scanner is the only thing that
    does.

BT appears inside strings, inside names, and inside an inline image's data,
and a byte search cannot tell those from an operator. The test pins the
discriminator:

(BT in a string) Tj  BT /F1 12 Tf ET
 ^ byte search: 1                      ← inside somebody's text
                     ^ the scan: 20     ← the operator

A tool splicing at index 1 corrupts the string.

Two details that are easy to get wrong

  • End is the lexer's position, not the operator's start plus its length.
    An inline image's operator token is BI and its operation runs to the EI
    three keywords later, so measuring from the operator would put the end in
    the middle of the image's data — where a splice corrupts the image instead
    of the stream around it. Asserted;
  • Start is the first operand's position, so splicing before an operation
    does not land between its operands and its operator. 1 0 0 RG starts at
    the 1, not at the RG.

The ranges of a stream are asserted ordered and disjoint, which is what
lets a caller splice at several of them in one pass, and a stream with rubbish
in it keeps honest ranges around what the scan stepped over.

Why in reader rather than in the caller

Three packages already scan content streams through this one scanner —
extract, render and latex — so the primitive is shared already; what was
missing was the one fact only the scan has. The first consumer is
go-pdfkit/ops, for a verb that recolours text without touching anything else.

Checks

100 % statement coverage, go vet and gofmt clean. Two mutations caught: the
range reduced to the operator alone, and the first-operand mark overwritten by
each later operand.

🤖 Generated with Claude Code

Operation gains Start and End: the byte range of the operation in the stream
it was scanned from, Start at its first operand (or at the operator when it has
none) and End just past the operator.

⛔ THEY ARE HERE SO THAT A CONTENT STREAM CAN BE EDITED IN PLACE. A tool that
wants to put something before every BT has two honest ways to find one:
re-serialise the whole stream, which risks changing what an inline image's
bytes mean, or know where the operator was — and this scanner is the only thing
that does know. "BT" appears inside strings, inside names and inside an inline
image's data, and a byte search cannot tell those from an operator. The test
pins the discriminator: in `(BT in a string) Tj BT /F1 12 Tf ET` a byte search
finds index 1, inside somebody's text, and the scan reports index 20.

End is the LEXER's position rather than the operator's start plus its length,
because an inline image's operator token is "BI" and its operation runs to the
EI three keywords later — measuring from the operator would put the end in the
middle of the image's data, where a splice corrupts the image instead of the
stream around it.

Start is the first OPERAND's position, so that splicing before an operation
does not land between its operands and its operator.

The ranges of a stream are asserted ordered and disjoint, which is what lets a
caller splice at several of them in one pass, and a stream with rubbish in it
keeps honest ranges around what the scan stepped over.

100% statement coverage, go vet and gofmt clean. Two mutations caught: the
range reduced to the operator alone, and the first-operand mark overwritten by
each later operand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 9f160d6 into main Oct 11, 2026
9 checks passed
@tannevaled
tannevaled deleted the where-an-operation-was branch October 11, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant