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
1 change: 1 addition & 0 deletions .github/race-weights
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ unlisted 0.58
117.9 TestFreeBoxesCarryPaddingOfAnyLength
99.9 TestEveryBase64TextSizeIsExact
93.8 TestAFilmPictureLargerThanAnyAV1LevelIsRefusedNotBroken
73.6 TestEveryAvifRungStillFitsInsideItsDeclaredCeilings
67.9 TestADeclaredDefaultIsTheOneTheFormatUses
64.3 TestNoTrackedFileCarriesPrivateContent
63.8 TestNoGeneratorHoldsTheWholeFileInMemory
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ because it turns other people's test suites red.
was meant. Only pixels along those edges change, and many files do not
change at all. A test that compares a file with one 0.5.0 generated from the
same request can fail.
- **A damaged file names its damage in its label.** The label now reads
`tfg - txt - 10485760 B - damage truncate:missing=50 - seed c2a547b2`: the
size the file has on disk, then every damage with its settings, in the order
they were applied. A long list of damages names the first few and how many
more there are, and the manifest lists every one. A file damaged with
`zero-head` and carrying a label is therefore not byte for byte what 0.5.0
wrote. Files with no damage, and files made with `--clean`, are unchanged.
- **An AVIF file just above the size where its picture grows gets the smaller
picture.** The picture now steps up to 160x120 at 3865 B, to 256x256 at
5493 B, to 320x240 at 7732 B and to 640x480 at 18722 B, about a tenth above
where it did. A file between the old step and the new one is not byte for
byte what 0.5.0 wrote.

### Added

Expand Down Expand Up @@ -164,6 +176,18 @@ because it turns other people's test suites red.

### Fixed

- **An AVIF file asked for just above the size where its picture grows is
written.** In 0.5.0 about a quarter of the files asked for at 16389 or
16439 B were not written at all, and the run ended with "1 file could not be
produced" although a preview had accepted them. The same happened, less
often, at the smaller steps.
- **A damage setting outside its range is refused on the command line and in
the window.** `--damage truncate:missing=95` cut the default half, wrote the
file, ended with code 0 and recorded `missing: 95` in the manifest, and
`--damage zero-head:bytes=99999999999` zeroed 8 bytes. A setting the damage
does not take, such as `truncate:foo=1`, went through as well. Now the run
ends with code 2 before anything is written and says what the setting takes,
and the window shows that under the field. A recipe was already refused.
- **An AVIF picture wider than 4096 px, or larger than 4096 x 2304 px, opens
now.** Every release since AVIF arrived in 0.3.0 wrote such a picture as a
file no viewer reads: Pillow, ImageMagick and libaom all refused it as
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,19 @@ tfg generate --format png --size 20kb --count 10 --damage zero-head:bytes=16
tfg generate --format zip --size 5mb --damage truncate:missing=30
```

A damaged file names its damage in its label, beside the size it has on disk:
`tfg - txt - 10485760 B - damage truncate:missing=50 - seed c2a547b2`. So two
texts cut by different shares can be told apart from their first line, although
the text after it is the same. A long list of damages names the first few and
how many more there are, and the manifest lists every one. The label is still
written before the damage, so the damage can overwrite it, and the manifest does
not promise it is there.

The smallest file a damage can be given follows its settings, so the column is
measured with the defaults. Ask for less and the run is refused before anything is
written, naming a size that would work.
written, naming a size that would work. A setting outside what the damage takes,
such as `truncate:missing=95`, is refused before anything is written as well, and
the run ends with code 2.

Asking for `--expected accept` beside a damage that breaks the rules of the
format is refused too, because nothing that follows them accepts the file. Where
Expand Down
25 changes: 23 additions & 2 deletions internal/cli/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,21 @@ func classifyRequest(err error) (int, bool) {
if errors.As(err, &unknownPreset) {
return ExitUsage, true
}
if code, ok := classifyDamage(err); ok {
return code, true
}
if code, ok := classifyFormat(err); ok {
return code, true
}
return 0, false
}

// classifyDamage covers what was asked to be broken.
//
// Its own function rather than more of classifyRequest, which crossed the
// line the crowding guard draws at sixty lines when the setting refusal
// arrived. Asked before classifyFormat, for the reason the last branch gives.
func classifyDamage(err error) (int, bool) {
// A damage this build does not know is a typo in the invocation, the same
// class as an unknown preset - a recipe naming one is refused while the
// recipe is read, so anything reaching here came off the command line.
Expand Down Expand Up @@ -288,8 +303,14 @@ func classifyRequest(err error) (int, bool) {
if errors.As(err, &unsized) {
return ExitUsage, true
}
if code, ok := classifyFormat(err); ok {
return code, true
// The same class once more: a setting a damage does not take, or a value
// it does not allow, is wrong for every format and size. Asked before
// classifyFormat on purpose, because the refusal it wraps is the one a
// format setting out of range ends with FORMAT for - and that one is a fact
// about the format, which this is not.
var setting *damage.SettingError
if errors.As(err, &setting) {
return ExitUsage, true
}
return 0, false
}
Expand Down
17 changes: 0 additions & 17 deletions internal/core/label.go

This file was deleted.

12 changes: 12 additions & 0 deletions internal/core/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ const (
// that is where the box is, and a bare width would collide with a recipe
// key of the same name the day one arrives.
KeyProperties = "properties"
// KeyDamage is the list of damages a target applies. A refusal about one
// setting of one entry is addressed under it - targets[1].damage[2].bytes
// - by the recipe reader and the engine alike, so a report names the same
// box whichever of them refused it.
KeyDamage = "damage"
)

// TargetAddress is where one setting of one target lives.
Expand Down Expand Up @@ -152,6 +157,13 @@ func ContentAddress(target, entry int, setting string) string {
return fmt.Sprintf("%s.%s[%d].%s", TargetPrefix(target), KeyContains, entry, setting)
}

// DamageAddress is where one setting of one damage lives inside a target,
// without the target: the engine puts that in front when it knows which
// target it is (atTarget). The position counts from one, for the reason above.
func DamageAddress(entry int, setting string) string {
return fmt.Sprintf("%s[%d].%s", KeyDamage, entry, setting)
}

// AddressNamesATarget says whether an address carries the position of the
// target it is about, rather than a setting name on its own.
//
Expand Down
112 changes: 106 additions & 6 deletions internal/damage/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"io"
"sort"
"strconv"
"strings"

"github.com/donislawdev/TestingFilesGenerator/internal/core"
"github.com/donislawdev/TestingFilesGenerator/internal/format"
Expand Down Expand Up @@ -92,12 +94,30 @@ func (d Descriptor) checkOne(p format.Property, raw string) error {
}
}

// Check is the first problem with the values stated, for the one-target path
// from the command line flags where there is no form to lay four of them out
// on.
func (d Descriptor) Check(v Values) error {
if bad := d.CheckEach(v); len(bad) > 0 {
return bad[0]
// Check is the first problem with what this chain states: a damage this build
// does not know, or a setting its declaration does not take or does not allow.
//
// The engine asks it, for the command line and the window, which hand it a
// chain without reading a recipe. The recipe reader asks CheckEach of every
// entry and reports each problem with its address, so this is a second caller
// of the same rule rather than a copy of it - the shape O199 gave the
// expectation. The first problem only, as format.Descriptor.CheckProperties
// gives it beside this: a window draws one damage at a time, and the engine
// addresses one refusal per target.
//
// Asked before a byte is written, because a value past the declaration does
// not fail later. The settings are read with a fallback to the default
// (missingShare, junkBytes, headBytes), so without this the file would be cut
// by the default and the manifest would record the value as written.
func (c Chain) Check() error {
for i, s := range c {
d, err := Get(s.ID)
if err != nil {
return err
}
if bad := d.CheckEach(s.Values); len(bad) > 0 {
return &SettingError{Position: i + 1, Err: bad[0]}
}
}
return nil
}
Expand Down Expand Up @@ -347,6 +367,86 @@ func (c Chain) String() string {
return fmt.Sprint(out)
}

// LabelLimit is the most bytes the damage takes up in a label.
//
// The label lands in fields with limits of their own, and the tightest one
// measured is a gzip comment, which Go's reader refuses from 512 bytes
// (compress/gzip, a 512 byte buffer). A chain has no length limit of its own,
// so without this a long one would break a format before any damage was
// applied - an archive Go cannot open, a ZIP comment past its padding. Bounded
// here, once, so a format has one length to be ready for rather than every
// length a recipe can write (TestALongDamageLabelLeavesEveryFormatWhole).
const LabelLimit = 160

// Label is the chain as the label of a damaged file states it: every entry in
// order, its settings resolved the way the manifest records them, joined by
// " then " - "truncate:missing=50 then append-junk:bytes=1024". Empty for an
// empty chain, which is a file whose label does not change.
//
// Resolved rather than as written, for the reason Resolved gives: the label
// answers what was done to this file, and its reader should not need the
// defaults of the build that wrote it. ASCII whatever a setting holds, because
// the label goes into encodings where nothing else is sure to survive
// (format/label.go): a value with anything else in it, or with a space or one
// of the separators, is written quoted with escapes.
//
// At most LabelLimit bytes. A chain longer than that names the entries that
// fit and how many more there are - "... and 12 more" - and the manifest lists
// every one. An entry too long to fit on its own is named by its damage alone.
func (c Chain) Label() string {
out := make([]string, 0, len(c))
for _, s := range c {
values := Values{}
for name, value := range s.Resolved() {
values[name] = plainOrQuoted(value)
}
out = append(out, Spec{ID: s.ID, Values: values}.String())
}
if whole := strings.Join(out, " then "); len(whole) <= LabelLimit {
return whole
}
return shortened(c, out)
}

// shortened is the entries of a chain that fit in LabelLimit with room left
// for saying how many did not.
func shortened(c Chain, entries []string) string {
kept := ""
for i, entry := range entries {
if i == 0 && len(entry) > LabelLimit/2 {
entry = c[0].ID
}
next := entry
if kept != "" {
next = kept + " then " + entry
}
if len(next)+len(moreThan(len(entries)-i-1)) > LabelLimit {
return kept + moreThan(len(entries)-i)
}
kept = next
}
return kept
}

// moreThan is what a shortened label says about the entries it leaves out.
func moreThan(n int) string {
if n == 0 {
return ""
}
return " and " + strconv.Itoa(n) + " more"
}

// plainOrQuoted is v as it is when every byte of it is printable ASCII other
// than the separators a label entry is written with, and quoted otherwise.
func plainOrQuoted(v string) string {
for i := 0; i < len(v); i++ {
if b := v[i]; b <= ' ' || b > '~' || b == ',' || b == '=' || b == ':' || b == '"' {
return strconv.QuoteToASCII(v)
}
}
return v
}

// Resolved is the settings this entry was applied with, defaults filled in.
//
// Filled in rather than left as written, because the manifest answers "what
Expand Down
38 changes: 37 additions & 1 deletion internal/damage/refusals.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package damage

import (
"errors"
"strings"

"github.com/donislawdev/TestingFilesGenerator/internal/core"
Expand Down Expand Up @@ -63,6 +64,41 @@ func (e *UnknownError) Said() core.Said {
core.A("What", e.what()), core.A("Known", strings.Join(e.Known, ", ")))
}

// SettingError is one entry of a chain stating a setting its damage does not
// take, or a value the declaration of that setting does not allow.
//
// A wrapper around the declaration's own refusal rather than that refusal on
// its own, for two things the refusal underneath cannot know. Which entry of
// the list it is about, so a form marks the right box and the engine names the
// address the recipe reader gives for the same mistake. And its class: a value
// a damage does not allow is wrong whatever format and size are asked for, so
// the command line ends with the code a mistyped flag gets, not the code for a
// request no format can deliver - the call O199 made for an expectation.
//
// The words are the declaration's (format.Property.Allows and Instead), reached
// through Unwrap, so a damage setting and a format setting refuse alike.
type SettingError struct {
// Position is the entry's place in the chain, counted from one.
Position int
// Err is *format.PropertyValueError or *format.UnknownPropertyError.
Err error
}

func (e *SettingError) Error() string { return e.Err.Error() }

func (e *SettingError) Unwrap() error { return e.Err }

// AboutSetting is the setting inside its entry - damage[1].missing. The
// engine puts the target in front, and a window showing one target keeps the
// last part, which is the name its box is registered under.
func (e *SettingError) AboutSetting() string {
var about interface{ AboutSetting() string }
if !errors.As(e.Err, &about) || about.AboutSetting() == "" {
return core.KeyDamage
}
return core.DamageAddress(e.Position, about.AboutSetting())
}

// TooSmallError is a file smaller than the damage it was given.
//
// It is a refusal in the plan rather than a failure part way through, because
Expand Down Expand Up @@ -234,7 +270,7 @@ type EatenError struct {

// AboutSetting puts this on the damage, because the order of the list is the
// thing to change.
func (e *EatenError) AboutSetting() string { return "damage" }
func (e *EatenError) AboutSetting() string { return core.KeyDamage }

func (e *EatenError) What() string { return e.what().String() }

Expand Down
16 changes: 11 additions & 5 deletions internal/engine/damage.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@ func checkDamage(t *Target, desc format.Descriptor) error {
// prepareDamage is what a damaged target is refused for before a size is
// drawn, and the format its files are planned through.
//
// Before the draw, because a chain that cuts away what it added and a damage
// with no size to work from are wrong whatever size comes out of the seed.
// And the format it hands back is the one the draw has to ask: a damage that
// changes the length needs the generator asked for a different size than the
// one on disk (damagedformat.go).
// Before the draw, because a setting the damage does not allow, a chain that
// cuts away what it added and a damage with no size to work from are wrong
// whatever size comes out of the seed. And the format it hands back is the one
// the draw has to ask: a damage that changes the length needs the generator
// asked for a different size than the one on disk (damagedformat.go).
//
// The settings first, because every question after them reads the settings
// and would answer about the default where the value is out of range.
func prepareDamage(t *Target, desc format.Descriptor) (format.Descriptor, error) {
if len(t.Damage) == 0 {
return desc, nil
}
if err := t.Damage.Check(); err != nil {
return desc, err
}
if eaten := t.Damage.Eaten(); eaten != nil {
return desc, eaten
}
Expand Down
Loading
Loading