Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,13 @@ because it turns other people's test suites red.
the length of the content is known. A cut in the padding also gets the
note `cut_in_padding`. In a compressed tar.gz a cut in the padding is
reported as a cut in the content.
- **`truncate` cuts a PNG's picture by the percentage you set.**
`--damage truncate:missing=90` on a 1 MB PNG used to cut only padding and
leave the whole picture. Now it removes 90% of the picture's content as
well, so a viewer that shows what is left shows only the top of the
picture, the way an interrupted upload leaves it. Files nothing cuts are
unchanged.
- **`truncate` cuts the picture of a PNG, a JPEG or an icon by the
percentage you set.** `--damage truncate:missing=90` on a 1 MB PNG used to
cut only padding and leave the whole picture, and on a JPEG or a large
icon it removed the whole picture whatever percentage you set. Now it
removes 90% of the picture's content, so a viewer that shows what is left
shows the part of the picture that arrived, the way an interrupted upload
leaves it. Files nothing cuts are unchanged.

- **tfg is on npm, for tests in Node.**
`npm install --save-dev testing-files-generator` gives Playwright, Vitest
Expand Down
53 changes: 53 additions & 0 deletions internal/format/cut.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package format

import (
"io"
"math/bits"
)

// PaddingInFront is how much padding goes in front of a file's content so that
// a cut after its first cutAt bytes keeps the same share of the content as of
// the whole file (Request.CutAt): cutAt, less the before bytes that stand in
// front of that padding, less content*cutAt/whole rounded down.
//
// It is the answer before a format rounds it to what it can write, and zero or
// less means that even with no padding in front the cut keeps no more than the
// share. Each format that splits its padding around its content for a cut asks
// this, so the share is one rule (docs/TRUNCATE-2026-11-11.md sections 7.4 and
// 8.5).
//
// content*cutAt is worked out in 128 bits, because a large picture times a cut
// near the top of what a file can be passes an int64. The quotient fits as
// long as 0 < cutAt < whole and content <= whole, which the caller sees to.
func PaddingInFront(before, content, cutAt, whole int64) int64 {
hi, lo := bits.Mul64(uint64(content), uint64(cutAt))
kept, _ := bits.Div64(hi, lo, uint64(whole))
return cutAt - before - int64(kept)
}

// TailHolder passes a stream on while keeping its last Keep bytes back, so a
// format can write something in front of them once the stream ends - padding
// in front of the end marker an encoder writes last.
type TailHolder struct {
W io.Writer
Keep int
// Tail is the bytes held back, Keep of them once that many have come.
Tail []byte
// Written is how many bytes went on to W.
Written int64
}

func (t *TailHolder) Write(p []byte) (int, error) {
n := len(p)
buf := append(t.Tail, p...)
if len(buf) > t.Keep {
emit := buf[:len(buf)-t.Keep]
if _, err := t.W.Write(emit); err != nil {
return 0, err
}
t.Written += int64(len(emit))
buf = buf[len(buf)-t.Keep:]
}
t.Tail = append([]byte(nil), buf...)
return n, nil
}
40 changes: 8 additions & 32 deletions internal/format/gif/gif.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,25 +337,25 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error {

// The trailer is always the final byte, so hold it back and let the rest
// stream out. Nothing else is buffered.
holder := &tailHolder{w: w, keep: trailerSize}
holder := &format.TailHolder{W: w, Keep: trailerSize}
if err := encode(holder, m); err != nil {
return err
}
if !m.bodyKnown {
// Planning skipped the encoding, so this is where the exact size
// arrives and the padding gets settled - the same arithmetic planning
// would have done with the same number.
m.body = holder.written
m.body = holder.Written
if err := settlePadding(&m, p.Bytes, m.body+trailerSize); err != nil {
// Unreachable unless ladderCeiling is wrong, and then saying so
// beats writing a file of the wrong length.
return core.Defect(fmt.Errorf("gif: %w - ladderCeiling is wrong", err))
}
markAfterThePicture(w, m, p.Bytes, false)
} else if holder.written != m.body {
return core.Defect(fmt.Errorf("gif: the picture encoded to %d B where planning said %d B", holder.written, m.body))
} else if holder.Written != m.body {
return core.Defect(fmt.Errorf("gif: the picture encoded to %d B where planning said %d B", holder.Written, m.body))
}
if holder.tail[0] != 0x3B {
if holder.Tail[0] != 0x3B {
return core.Defect(fmt.Errorf("gif: the encoded stream does not end with the trailer"))
}

Expand All @@ -365,7 +365,7 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error {
}
}

_, err := w.Write(holder.tail)
_, err := w.Write(holder.Tail)
return err
}

Expand Down Expand Up @@ -512,11 +512,11 @@ func chooseSize(r format.Request, label string) (memo, error) {
}

func encodedBodySize(m memo) (int64, error) {
holder := &tailHolder{w: io.Discard, keep: trailerSize}
holder := &format.TailHolder{W: io.Discard, Keep: trailerSize}
if err := encode(holder, m); err != nil {
return 0, err
}
return holder.written, nil
return holder.Written, nil
}

// minimumBytes is the smallest file this generator can produce: a one pixel
Expand All @@ -533,27 +533,3 @@ func minimumBytes() int64 {
}
return body + trailerSize
}

// tailHolder passes bytes through while keeping the last keep bytes back, so
// the caller can insert something in front of them.
type tailHolder struct {
w io.Writer
keep int
tail []byte
written int64
}

func (t *tailHolder) Write(p []byte) (int, error) {
n := len(p)
buf := append(t.tail, p...)
if len(buf) > t.keep {
emit := buf[:len(buf)-t.keep]
if _, err := t.w.Write(emit); err != nil {
return 0, err
}
t.written += int64(len(emit))
buf = buf[len(buf)-t.keep:]
}
t.tail = append([]byte(nil), buf...)
return n, nil
}
65 changes: 50 additions & 15 deletions internal/format/ico/ico.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// An ICO is a directory of images rather than an image, so it carries an
// explicit offset saying where each one starts. That offset is the padding
// channel: anything between the directory and the picture is space the file
// describes.
// describes. A file a damage will cut has part of that space behind the
// picture instead, where no entry points either, so the cut takes the share
// of the picture asked for (frontGap).
//
// The picture inside can be a device independent bitmap or a whole PNG, and
// both are in use. This generator writes either, because which one a system
Expand Down Expand Up @@ -105,8 +107,12 @@ type memo struct {
// image is the picture as it will sit inside the file. Small by
// construction, because an icon side is at most 256 pixels.
image []byte
// gap is how many bytes stand between the directory and that picture.
// gap is how many bytes of padding the file carries: all of them between
// the directory and the picture, unless a damage will cut the file.
gap int64
// cutAt is where a damage will cut the file, zero when nothing does
// (format.Request.CutAt).
cutAt int64
}

func (generator) Plan(r format.Request) (format.Plan, error) {
Expand Down Expand Up @@ -137,7 +143,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) {

m := memo{
width: w, height: h, seed: r.Seed, label: label, embed: embed,
image: blob, gap: r.Bytes - bare,
image: blob, gap: r.Bytes - bare, cutAt: r.CutAt,
}

p := format.Plan{
Expand Down Expand Up @@ -250,25 +256,54 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error {
default:
}

// The map, all of it known from the plan: the directory, the gap that
// makes up the size, and the picture. The gap stands BEFORE the picture,
// so a cut takes the whole picture whatever share is asked for
// (docs/TRUNCATE-2026-11-11.md section 2).
// The map, all of it known from the plan: the directory, the gap in front
// of the picture, the picture, and the rest of the gap behind it when a
// damage will cut the file.
front := frontGap(m, p.Bytes)
picture := int64(len(m.image))
format.Mark(w, format.RoleStructure, 0, preamble)
format.Mark(w, format.RolePadding, preamble, m.gap)
format.Mark(w, format.RoleContent, preamble+m.gap, int64(len(m.image)))
format.Mark(w, format.RolePadding, preamble, front)
format.Mark(w, format.RoleContent, preamble+front, picture)
if back := m.gap - front; back > 0 {
format.Mark(w, format.RolePadding, preamble+front+picture, back)
}

if err := writeDirectory(w, m); err != nil {
if err := writeDirectory(w, m, front); err != nil {
return err
}
if err := writeGap(ctx, w, m.seed, m.gap); err != nil {
if err := writeGap(ctx, w, m.seed, front); err != nil {
return err
}
_, err := w.Write(m.image)
return err
if _, err := w.Write(m.image); err != nil {
return err
}
return writeGap(ctx, w, m.seed, m.gap-front)
}

// frontGap is how much of the gap stands between the directory and the
// picture: all of it, unless a damage will cut the file
// (format.Request.CutAt). Then as much as puts the cut where it keeps the
// share of the picture it keeps of the whole file, and the rest goes behind
// the picture.
//
// With all of the gap in front, a large icon cut at any share lost the whole
// picture. Bytes behind the picture are space no entry points at, as the gap
// is, and Pillow, FFmpeg, ImageMagick, the Windows Imaging Component and
// GDI+ return the same pixels either way (docs/TRUNCATE-2026-11-11.md
// section 8.3). The gap has no header, so the cut lands on the byte - except
// with too small a gap, where none of it goes in front and the cut takes more
// of the picture, the "at least" missing promises. It never needs more than
// the whole gap in front (section 8.5).
func frontGap(m memo, whole int64) int64 {
if m.cutAt <= 0 || m.cutAt >= whole {
return m.gap
}
return max(0, format.PaddingInFront(preamble, int64(len(m.image)), m.cutAt, whole))
}

func writeDirectory(w io.Writer, m memo) error {
// writeDirectory writes the directory of one entry, whose picture starts
// front bytes after it.
func writeDirectory(w io.Writer, m memo, front int64) error {
var head [preamble]byte
// Two reserved bytes stay zero, then the type: 1 means icon.
binary.LittleEndian.PutUint16(head[2:4], 1)
Expand All @@ -281,7 +316,7 @@ func writeDirectory(w io.Writer, m memo) error {
binary.LittleEndian.PutUint16(e[4:6], 1)
binary.LittleEndian.PutUint16(e[6:8], 32)
binary.LittleEndian.PutUint32(e[8:12], uint32(len(m.image)))
binary.LittleEndian.PutUint32(e[12:16], uint32(int64(preamble)+m.gap))
binary.LittleEndian.PutUint32(e[12:16], uint32(int64(preamble)+front))

_, err := w.Write(head[:])
return err
Expand Down
129 changes: 129 additions & 0 deletions internal/format/jpg/cut.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package jpg

import (
"bytes"
"context"
"fmt"
"io"

"github.com/donislawdev/TestingFilesGenerator/internal/core"
"github.com/donislawdev/TestingFilesGenerator/internal/format"
)

// writeForACut writes a file a damage will cut after its first m.cutAt bytes,
// with the padding split around the picture so that the cut keeps the share of
// the picture it keeps of the whole file (format.Request.CutAt).
//
// With all of the padding in front of the picture, as an uncut file has it, a
// cut of any share took the whole picture. Behind the picture JPEG has one
// place for bytes that mean nothing: fill bytes, X'FF', which ITU T.81 B.1.1.2
// lets stand in front of any marker - here the end of image marker. A comment
// behind the last scan is outside the syntax (B.2.1), and bytes after the end
// marker are outside the picture. Pillow, FFmpeg, ImageMagick, Go's own
// decoder, the Windows Imaging Component and GDI+ return the same pixels
// either way (docs/TRUNCATE-2026-11-11.md section 8.3).
//
// The fill is in no file anyone is handed: the cut falls in the picture, and
// the generator stops there. Only a whole file written for a guard has it.
func writeForACut(ctx context.Context, w io.Writer, m memo, whole int64) error {
pad := whole - m.body
front := frontPadding(pad, m.body-soiSize-eoiSize, m.cutAt, whole)
markAroundThePicture(w, m.body, front, pad-front)

if _, err := w.Write([]byte{0xFF, 0xD8}); err != nil {
return err
}
if front > 0 {
segments, payload := segmentsFor(front)
if err := writeComments(ctx, w, m.seed, segments, payload); err != nil {
return err
}
}

// The encoder's own start marker is dropped, as in Write, and its end
// marker held back, so the fill goes in front of it.
holder := &format.TailHolder{W: w, Keep: eoiSize}
skip := &headSkipper{w: holder, keep: soiSize}
if err := encode(skip, m); err != nil {
return err
}
if err := skip.check(); err != nil {
return err
}
if skip.written+soiSize != m.body {
return core.Defect(fmt.Errorf("jpg: the picture encoded to %d B where planning said %d B", skip.written+soiSize, m.body))
}
if !bytes.Equal(holder.Tail, []byte{0xFF, 0xD9}) {
return core.Defect(fmt.Errorf("jpg: the encoded stream ends with % x, not the end of image marker", holder.Tail))
}
if err := writeFill(ctx, w, pad-front); err != nil {
return err
}
_, err := w.Write(holder.Tail)
return err
}

// frontPadding is how much of the padding, comment headers included, goes in
// front of the picture, so that a cut after the first cutAt bytes of a whole
// file keeps content*cutAt/whole bytes of the picture's content.
//
// A comment costs four bytes before any padding, so the front is nothing or at
// least that. Where it cannot land exactly it grows, which moves the picture
// later and cuts at most three bytes more of it - the "at least" missing
// promises. The fill behind the picture has no header, so any length is
// possible there. The one way the cut can keep more is padding too small for
// the front it should have: all of it goes in front, and the cut keeps at most
// two bytes more than the share, because the end marker counts in the file and
// not in the content (docs/TRUNCATE-2026-11-11.md section 8.5). The manifest's
// cut reports what was kept either way.
func frontPadding(pad, content, cutAt, whole int64) int64 {
front := format.PaddingInFront(soiSize, content, cutAt, whole)
switch {
case front <= 0:
return 0
case front < comOverhead:
front = comOverhead
}
return min(front, pad)
}

// markAroundThePicture marks the map of a file whose padding is split around
// its picture: the start marker, the comments in front, the picture, the fill
// behind and the end marker. A side with no padding has no region.
func markAroundThePicture(w io.Writer, body, front, back int64) {
format.Mark(w, format.RoleStructure, 0, soiSize)
at := int64(soiSize)
if front > 0 {
format.Mark(w, format.RolePadding, at, front)
at += front
}
format.Mark(w, format.RoleContent, at, body-soiSize-eoiSize)
at += body - soiSize - eoiSize
if back > 0 {
format.Mark(w, format.RolePadding, at, back)
at += back
}
format.Mark(w, format.RoleEnd, at, eoiSize)
}

// writeFill writes n fill bytes, X'FF', in pieces of padChunkSize so that
// cancellation is noticed.
func writeFill(ctx context.Context, w io.Writer, n int64) error {
if n <= 0 {
return nil
}
buf := bytes.Repeat([]byte{0xFF}, int(min(n, padChunkSize)))
for remaining := n; remaining > 0; {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
k := min(remaining, int64(len(buf)))
if _, err := w.Write(buf[:k]); err != nil {
return err
}
remaining -= k
}
return nil
}
Loading
Loading