diff --git a/CHANGELOG.md b/CHANGELOG.md
index 653cf27..a4244e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,8 @@ because it turns other people's test suites red.
## [Unreleased]
+## [0.3.0] - 2026-09-09
+
### Breaking
- **WAV files asking for 24 or 32 bit audio have slightly different bytes.** A
@@ -56,6 +58,138 @@ because it turns other people's test suites red.
same new code and were checked against their recorded hashes and across every
format at five sizes and two seeds.
+- **A generated `.csv` quotes only the fields that need it, so its bytes are
+ different.** Sizes are unchanged. Every size that worked before still works,
+ every reader that took these files still takes them, and the file is still
+ RFC 4180.
+
+ The description column used to be quoted on every row. It is quoted now only
+ when it carries the separator, which is the one thing that makes a quote
+ necessary. The description is three to seven words and drops a separator
+ every third one, so a short one carries none - measured on a 4 kB table,
+ **9 of its 44 rows** lost their quotes.
+
+ This arrives as a new setting, `quote_style`, which takes `minimal`, `all` or
+ `none`:
+
+ - `minimal` is the new default and is what a spreadsheet writes.
+ - `all` wraps every field on every row, the header included.
+ - `none` wraps nothing. It also stops the description carrying the separator,
+ because an unquoted field cannot hold one without ending early - so this
+ value changes what the file says and not only how it is punctuated.
+
+ **The smallest `.csv` is 115 B rather than 117**, because the shortest row
+ has an empty description and an empty field needs no quotes. With
+ `quote_style=all` the smallest is 139 B. `tfg formats` prints the current
+ numbers.
+
+ **A suite pinning `.csv` hashes will go red once and then stay green.** There
+ is no switch back to the old bytes: they were not any of the three styles RFC
+ 4180 describes, and carrying a fourth name for them forever costs more than
+ the one red run.
+
+- **Six formats have different bytes, because the tool is built with Go 1.27
+ now.** Sizes are unchanged. Every size that worked before still works, every
+ reader that took these files still takes them, and the same sizes are
+ reachable. What changed is the compressed data inside them.
+
+ Affected: `targz`, `png`, `docx`, `xlsx`, `pptx`, `ico` when it holds a png,
+ and `zip` when you ask for compression. `zip` left alone is untouched,
+ because its default stores rather than compresses. The other seventeen
+ formats are byte for byte what they were.
+
+ Go 1.27 changed `compress/flate`, which is what all of those run through.
+ Below the default compression level the change is only how a stream is
+ closed, and above it the compressor itself behaves differently.
+
+ Two minimums moved with it. The smallest `png` is **74 B** rather than 73,
+ and sizes 75 to 82 and 85 are the ones it cannot produce. The smallest
+ `targz` is **1049 B** rather than 1052, the next size up is 1051, and 1050 is
+ the one it cannot produce. `tfg formats` prints the current numbers.
+
+ **A suite pinning hashes for those formats will go red once and then stay
+ green.** There is no switch back: staying on the old compiler was not a
+ choice this tool can offer, since the compiler comes from whoever builds it.
+
+- **`.tar.gz` could not be produced at all under Go 1.27 until this release.**
+ Every size was refused with an error saying the generator produced three
+ bytes fewer than planned. The size of a `.tar.gz` is worked out rather than
+ measured - compressing twice to learn a length would make a preview cost what
+ the run costs - and that arithmetic carried a number that turned out to
+ describe one release of Go.
+
+ It measures that number now, at first use, and checks its own answer before
+ trusting it. A later Go release can move these bytes again, but it can no
+ longer stop the format from being written.
+
+- **A generated `.tar.gz` has different bytes, because a lot of them could
+ not be opened by a Go program.** Sizes are unchanged, every size that
+ worked before still works, and every reader that took these files still
+ takes them. What moved is where the padding sits inside the header.
+
+ The padding used to ride in the gzip header comment. Go's own
+ `compress/gzip` reads that field into a fixed buffer and refuses a comment
+ of 512 bytes or more, so **4134 of the 11 260 reachable sizes produced an
+ archive no Go program could open** - and the message it gives,
+ `gzip: invalid header`, reads like a corrupt file rather than like a field
+ the reader will not take. 7-Zip, GNU tar, bsdtar, Python and node all took
+ those files without a word, which is why it was not noticed sooner.
+
+ The padding now rides in the gzip extra field, which Go reads to the end
+ of. After the change: 11 260 sizes reachable, none unreachable, none
+ unreadable.
+
+ **There is no way back to the old bytes**, and that is the difference
+ between this and the other two entries here. The old bytes are the ones a
+ Go program cannot read, so keeping a switch for them would be keeping a
+ switch for the fault. A suite pinning `.tar.gz` hashes will go red once
+ and then stay green.
+
+- **A generated log now advances through time, so its bytes are different.**
+ Every entry used to carry the same instant. Ten thousand requests all landing
+ at one moment is not a log anybody can test a time window, a rate alert or a
+ rotation against, and it was obvious the moment you looked at a file.
+
+ Entries are now one second apart by default, and `rate` sets how many arrive
+ a second. The bytes of every log change, so a suite pinning their hashes will
+ go red.
+
+ **The way back is `--set timestamps=fixed`**, or `timestamps: fixed` on a
+ target in a recipe. That holds the clock still and writes the same bytes this
+ tool wrote before, to the byte - there is a pinned hash proving it.
+
+- **A generated GIF now moves, so its bytes are different.** A GIF is the one
+ picture format here that can hold more than one frame, and a still one told
+ you nothing about how the system under test treats an animation - whether it
+ keeps it, flattens it to the first frame, or re-encodes it. Every GIF now
+ carries a marker that travels across the picture in three frames, and the
+ manifest says `animated` and `frame_count` for each file.
+
+ Two things change with it. The smallest GIF this tool will write goes from
+ 41 B to 114 B, because the number a format announces as its minimum has to be
+ a number a plain run accepts, and a plain run animates. And the bytes of every
+ GIF change, so a suite pinning their hashes will go red.
+
+ **The way back is `--set frames=1`**, or `frames: 1` on a target in a recipe.
+ That takes the plain encoder and writes the same bytes this tool wrote before,
+ to the byte - there is a pinned hash proving it.
+
+- **A setting value is spelled the way the format declares it.** `--set
+ page_size=A4` and `--set directory_entries=TRUE` used to be accepted and now
+ refuse with exit 4, naming the setting and the value. Write `a4` and `true`.
+
+ Nothing else changes: the bytes of every file are what they were, and a
+ recipe writing `header: true` is unaffected, because a YAML boolean arrives
+ as `true` either way. Only a value quoted into another case is refused.
+
+ It is a breaking change for a fourth of a reason and a fix for the rest. A
+ value the declaration did not contain used to pass the check and land on the
+ format, which then did one of four things with it: refuse in its own words,
+ understand it anyway, **quietly ignore it and produce the default file**, or
+ read it as something else entirely. `--set entry_owner=USER` on a `targz`
+ wrote an archive owned by nobody and reported success. Now nothing gets that
+ far.
+
### Added
- **A new command says what this build can break: `tfg damage`.**
@@ -213,248 +347,497 @@ because it turns other people's test suites red.
fewer records in the same number of bytes. The smallest document each layout
can produce differs too, and asking for less names the layout it is about.
-### Security
-
-- **On Windows, the desktop window loads the library it uses for dark menus from
- the system directory rather than by name.** Asking for `uxtheme.dll` by name
- goes through the standard Windows search order, and the directory the program
- was started from comes before the system one in that order - so a file of that
- name left beside a downloaded `tfg-gui.exe` would have been loaded into the
- program and run.
+- **`csv` takes `columns`, so a table can be as narrow or as wide as the thing
+ you are testing.** Two to 32768, and six by default - which is the six
+ columns this tool has always written, so a `.csv` you already generate does
+ not change by a byte.
- Nothing about the window changes. The menu is still dark, which was checked by
- asking Windows on a real machine rather than by reading the code.
+ Fewer than six drops them from the middle: `id` stays first and
+ `description` stays last, because that is the field stretched to reach the
+ exact size you asked for. More than six adds `field_7`, `field_8` and so on
+ in front of the description.
-- **The password of a locked archive is no longer repeated in the recorded
- command line.** It appeared twice in the manifest: under the file's own
- `properties`, where it is written on purpose because a locked fixture nobody
- can open checks nothing, and again inside `run.command`, which records the
- whole command line as typed.
+ **Above 16384 columns a spreadsheet quietly keeps the first 16384 and drops
+ the rest.** Measured with LibreOffice Calc: 16384 comes back whole, 16385
+ comes back with one column missing and no warning anywhere. The ceiling here
+ is deliberately past that, so you can build the set either side of the line
+ rather than only the last file that survives it.
- The second one was a side effect. `run.command` is the line people copy - into
- a bug report, into a README, into a commit beside a set of fixtures - and it
- reads like metadata rather than like fixture data, so it was not treated with
- the same care. It now reads `--set password=***`. The deliberate copy is
- untouched, so nothing that opens these archives changes.
+ The smallest file moves with the setting, the way it already does for row
+ endings and quoting - 36 B at two columns, 115 B at six, 5017 B at 256.
+ `tfg formats csv` prints what a given table needs.
- **If you compare `run.command` between runs, that string is different now.**
+- **A `targz` manifest says what its entries claim about themselves.** Two new
+ keys on the file entry, `entry_mode` and `entry_owner`, written every run
+ rather than only when you ask for them, so a harness never has to read a
+ missing key as "nobody owns this". The manifest schema version is unchanged.
- A manifest that carries a password is also written `0600` rather than `0644`,
- so it is readable by its owner rather than by every account on the machine.
- Every other manifest, and every generated file, keeps the mode it had - this
- tool exists to produce files somebody else's CI will read. Windows has no
- permission bits, so nothing changes there.
+- **A CSV can be written in the dialect you were handed.** `--set
+ delimiter=semicolon`, `--set line_ending=crlf` and `--set header=false` on a
+ `csv`, separately or together. Separators are named rather than typed, so
+ `tab` and `pipe` need no escaping: the four are `comma`, `semicolon`, `tab`
+ and `pipe`.
-- **Building the program yourself now needs the build tag, and says so if you
- leave it out.** The AVIF encoder has an assembly path that reads past the end
- of a buffer and takes the process down on some picture sizes. Every workflow
- here has passed the tag that turns it off since the encoder arrived - and
- none of the three ways `README.md` offered for building it yourself did, so
- `go install`, a distribution package and a build from a checkout all got the
- path this project describes as reading outside its buffer.
+ These are the three ways a real CSV differs before its contents do. A
+ European spreadsheet exports with semicolons, anything written on Windows
+ ends its rows with CRLF, and a table dumped straight out of a database has no
+ header. All three are CSV and all three break a reader that assumed the other
+ thing.
- A build without the tag now stops at the compiler with a message naming it,
- rather than producing a binary that works until it meets the wrong picture
- size. Install with:
+ The description column keeps carrying the separator, so a semicolon file
+ still exercises quoted fields rather than quietly testing less than a comma
+ one does.
- ```
- go install -tags noasm github.com/donislawdev/TestingFilesGenerator/cmd/tfg@latest
- ```
+ Two things worth knowing. The smallest file changes with the dialect, because
+ a CRLF row is a byte longer and a header is a whole line - the tool tells you
+ the floor for the settings you gave it. And the manifest records the
+ separator as the character that is in the file, where the recipe names it as
+ a word.
- The files the program produces are the same either way, so nothing you have
- generated changes.
+ The defaults are `comma`, `lf` and a header, which is what this tool has
+ always written, so **no existing file changes by a byte**.
-- **A recipe that nests lists deeply is refused before it is read, whichever way
- it is written.** The check that already refused deeply nested brackets counted
- only brackets, and the same nesting written as `- - - - x` costs two bytes a
- level and carries none.
+- **A log can be made quiet, or full of errors.** `--set level_mix=errors` on
+ a `log`, with `realistic`, `quiet`, `errors` and `debug` to choose from. It
+ decides which severities appear, the way `status_mix` already decides which
+ response codes do.
- Measured against the previous build. A 500 kB recipe of nothing but dashes ran
- for 88 seconds and ended in `fatal error: out of memory` with a page of Go
- internals on standard error, leaving with the exit code a mistyped flag gets.
- At 1 MB - the largest recipe the size limit allows - it ran for 70 seconds and
- said nothing at all. The same file is now refused in under a second, with a
- message naming the depth and the limit.
+ Only the `plain` and `json-lines` entry formats carry a severity at all. Ask
+ for a mix beside one of the other four and the tool says so and stops,
+ naming both settings, rather than accepting a setting that would do nothing.
- The limit counts how deeply lists and mappings nest, in either style, and it
- is 32. An ordinary recipe reaches one, and an archive declaring what it
- contains reaches two.
+ One thing worth knowing before you pick `quiet`: it draws only `INFO`, which
+ is a shorter word than `ERROR`, so the smallest log it can write is one byte
+ smaller than the other mixes. The tool tells you the floor for the settings
+ you gave it.
-- **A file is never written under a name something else already holds.** Every
- file this tool writes goes to a temporary name first and is renamed into
- place. Three of those temporary names were created in a way that follows a
- link, so a link left at one of them by somebody else sent the bytes wherever
- it pointed - outside the directory you gave - and the run still reported
- success.
+ The default is `realistic`, the mix these logs have always had, so **no
+ existing file changes by a byte**.
- Reproduced against the previous build. A link at the manifest's temporary name
- put the manifest onto a file outside the output directory and exited 0, after
- which `verify` called that run a match and `cleanup` reported it removed. The
- same shape made `recipe fmt -w` write your recipe onto somebody else's file
- and leave your recipe itself as a link. On Windows none of this needs a
- privilege, because a hard link is enough.
+- **An archive can compress what it holds.** `--set compression=best` on a
+ `zip` or a `targz`, with `none`, `fast`, `default` and `best` to choose from.
- Every one of those names is now claimed rather than created, and a name
- something else holds is a refusal that says which name and what to do.
- Pointing `--out` at a directory reached through a link keeps working, which is
- the setup this was measured against.
+ The archive still comes out **exactly the size you asked for**. What changes
+ is how much of it is your files and how much is padding: at `best` a
+ megabyte archive holding four 32 KB text files carries the same four files
+ deflated, and the padding entry grows to make up the difference. A reader
+ sees real deflated entries, which is what a tool under test has to cope with.
- **What changes for an ordinary run: nothing.** The one case you can meet
- without somebody working against you is a leftover `.tfg-writing` file from a
- run that was killed part way through. That used to be written over in silence.
- It is now a refusal naming the file, so remove it and run again.
+ The default is `none`, which is what archives from this tool have always
+ been, so **no existing file changes by a byte**.
-### Changed
+ Two combinations are refused rather than half-supported, and the message
+ says which two settings to choose between. Compression with a size taken
+ **from the contents**: the archive's length would then be whatever the
+ contents compress to, which is only knowable by compressing them, and that
+ would make a preview cost as much as the run. Compression with a
+ **password**: a locked entry has to state its length before its data is
+ written, so a compressed one would have to be held in memory whole.
-- **Two commands now refuse a name they cannot answer about, instead of
- quietly answering about a different one.**
+ Compressing costs time at write, not at preview. A 10 MB archive takes about
+ 25 ms at `fast` and 140 ms at `default`, against 8 ms stored, and a `.tar.gz`
+ pays that twice because gzip compresses the whole stream and the size has to
+ be measured before it can be hit.
- ```
- tfg formats png svg described png and said nothing about svg, ending with 0
- tfg preset list some-name printed the whole list and ignored the name, ending with 0
- ```
+- **An archive can hold its files in directories.** `--set depth=3` puts every
+ file three levels down, and `--set directory_entries=true` also makes the
+ archive list the directories themselves. Both work on `zip` and on `targz`.
- Both now end with 2 and name the word they could not use. A script that
- asked about the wrong thing was getting a confident answer about something
- else, which is worse than being told no.
+ Two settings rather than one, because they are two questions. Depth is about
+ the paths inside. Directory entries are about whether the archive names the
+ directories at all - and extractors differ there: some create a directory
+ when they meet a path that needs one, and some create only what the archive
+ names. An archive is the one format where you can test both.
- `tfg preset list` takes no name at all, so anything after it was always a
- mistake - most likely somebody reaching for `tfg preset show`.
- The refusal says so.
+ The default is flat, which is what archives from this tool have always been,
+ so **no existing file changes by a byte**. Asking for `directory_entries`
+ without a depth is refused rather than quietly ignored: a flat archive has no
+ directories to name, and the message says so and names both settings.
- Nothing changes when you pass the right number of names, and no generated
- file moves a byte.
+ Depth goes up to 50. The limit is measured rather than picked: a `.tar.gz`
+ writes USTAR headers, which carry a path in a 155 byte prefix and a 100 byte
+ name split on a slash, and past a certain length no split works. Directories
+ cost 512 bytes each in a `.tar.gz` and about 76 plus the path in a `.zip`.
+ The size you order is still the size you get, to the byte.
-- **A spreadsheet can now be built wider than a spreadsheet can open.** The
- `columns` setting of `xlsx` used to stop at 64. It now reaches 32768, which is
- the ceiling `csv` already had.
+ The padding entry stays at the top of the archive rather than moving into the
+ directories, so you can always tell it apart from the files you asked for.
- The number matters because of where a reader stops. Excel and LibreOffice
- Calc both hold 16384 columns. Measured with Calc: a sheet of 16384 columns
- opens whole, and a sheet of 16385 opens as 16384 - the last column is dropped
- and nothing is said about it. At a ceiling of 64 there was no way to build a
- file that asks a spreadsheet about its own limit, which is the kind of file
- this tool exists to produce.
+- **A zip can be locked with ZipCrypto, the old scheme.** `--set encryption=zipcrypto`.
- `rows` times `columns` still cannot pass 2 million cells, so a sheet 16385
- columns wide holds up to 122 rows. Nothing about a sheet of 64 columns or
- fewer changes, and the default is still one column.
+ It is here for what it does to a reader rather than for what it protects. Measured: .NET's own `ZipFile` opens one of these, reports the entry at its true length, hands back a stream and fills it with the ENCRYPTED bytes - and never says the entry was encrypted at all. An application built on that library processes noise and calls it data. AES fails loudly in the same library, which is the safer defect and the less interesting one.
-- **Byte counts are grouped in threes.** A total used to print as
- `2516582400 B`. It now prints as `2 516 582 400 B`, in every message that
- names a number of bytes - `tfg formats`, the summary a run prints, what a
- preset says its budget is, and what `tfg validate` reports.
+ So this is the fixture for finding out whether something in a pipeline waves an encrypted archive through.
- Grouped with a space rather than a comma, because a comma is a thousands mark
- in some countries and a decimal point in others, and this tool is read in
- both.
+ **It is not protection and it is not offered as any.** ZipCrypto has been broken for decades. Use `aes-256` when the point is that the contents are hard to read.
- Machine output is untouched. `--json` and the manifest carry numbers rather
- than sentences, so nothing that parses those sees any of this. If you have a
- script reading a byte count out of the human output, it needs to take the
- spaces out.
+- **A zip can be locked with a password.**
-- **Notes are reported once per thing they say, not once per file.** A run of
- 25 000 one-byte text files used to print 25 001 `note:` lines, every one of
- them the same sentence about the label not fitting. It now prints one, with
- the count and the first three names:
+ tfg generate --format zip --size 30kb --set entries=3 \
+ --set password=Secret123 --set encryption=aes-256
- ```
- note: 25000 files: The label needs 32 B and the file is 1 B, so this file
- carries no label. Its name and the manifest still identify it. Named:
- files_0001.txt, files_0002.txt, files_0003.txt. 24997 files not named here.
- ```
+ writes an archive of exactly 30720 B that 7-Zip opens with that password
+ and refuses without it. The methods are `aes-128`, `aes-192` and
+ `aes-256`.
- A note about a single file still leads with that file's name, unchanged.
+ **The password goes into the manifest in plain text.** A locked fixture
+ nobody can open is worth nothing, so the manifest records it exactly as
+ you typed it - that is the point rather than a leak. Do not use a password
+ you use anywhere else.
- The reason this matters beyond tidiness: a run whose manifest will be too big
- for this build to read back warns you first, in a line that looked exactly
- like the 25 000 that followed it. That warning is the only thing standing
- between you and a directory that `verify` and `cleanup` can never read.
+ Both settings are needed together. A password with `encryption=none`, or
+ an encryption with no password, is refused rather than guessed at, and the
+ refusal names both of them.
- **Nothing in the manifest changes.** Every entry still carries its own note,
- where a machine reads it and nothing scrolls.
+ **Limits worth knowing before you build a fixture.** Some readers cannot
+ open AES archives at all - .NET's own `ZipFile` lists the entries and then
+ fails on reading one. Nothing in this build writes the older ZipCrypto
+ scheme yet, so an archive meant for a reader that only speaks that is not
+ something this can make. And `tar.gz` cannot be locked at all - neither
+ tar nor gzip has any encryption in it, and asking for one there is refused
+ with that reason rather than ignored.
-- **Building from source now needs Go 1.27.0.** It used to say 1.26.5, and that
- sentence was true of compiling and false of the product. Go 1.27 changed
- `compress/flate`, so a copy built on 1.26 answers the same version number,
- writes that number into every manifest, and produces different bytes for PNG,
- DOCX and TAR.GZ than the release of that name. The compiler now refuses that
- build instead of producing one that quietly disagrees with everybody else's.
+- **A tar.gz can say what permissions its files have and who owns them.**
+ `--set entry_mode=755` and `--set entry_owner=root`. The modes are the
+ ones chmod takes, from `000` through `777`, and the owners are `unset`
+ (the default, and what this tool has always written), `root` and `user`.
- If you are pinned to Go 1.26 you can no longer build from source. The
- downloadable binaries are unaffected, and so is `go install`.
+ The useful cases are the ones nobody makes by accident: `000` is a file
+ nothing can read after unpacking, `777` is one a scanner should have
+ something to say about, and an archive claiming root owns everything is
+ what a careless extractor turns into a privilege problem.
- A manifest already records which Go produced it, under `tool.go`, so two runs
- that disagree can be told apart after the fact as well.
+ It changes no bytes unless you ask for it, and the size of the archive is
+ the same either way.
-- **The `encryption` setting now says that a locked archive is a fixture rather
- than protection.** Nothing about the files changes - the sentence `tfg formats
- zip` prints, and the one the window shows beside the field, gained the two
- facts that were missing: the key is worked out from the run seed, so the same
- recipe gives the same archive on every machine, and the password is written
- into the manifest beside the file.
+- **A log can now be six shapes rather than one, and seven settings shape it.**
+ `tfg generate --format log --set entry_format=nginx` writes an nginx access
+ log. The others are `apache-combined` (the default, and what this format has
+ always written), `apache-common`, `syslog`, `plain` and `json-lines`.
- Both are deliberate and both are what makes these archives useful for testing
- a reader. `aes-256` means something else everywhere else it is written, which
- is why it is now said out loud. `README.md` says it too.
+ Every template was taken from a real file rather than from a specification
+ remembered: a real nginx and a real Apache, and rsyslog on a real machine. Two
+ of them would have been wrong otherwise. An nginx line carries one more
+ quoted field than "combined" does, and Apache's own default is `common`, with
+ no referrer and no agent at all.
-- **Files are written over several threads, so a run of many files is several
- times faster.** They used to be written one after another.
+ The rest of the settings: `timestamps` and `rate` for the clock, `methods` for
+ which verbs appear, `status_mix` for which response codes, `ip_version` to put
+ IPv6 addresses in front of a reader that may not expect them, and
+ `line_ending` for a log written by a Windows service.
- Nothing about what you get changes. The files are byte for byte identical,
- the manifest lists them in the same order, `verify` and `cleanup` behave
- exactly as before, and every refusal says what it said.
+ **A setting that could not do anything is refused rather than ignored.**
+ Asking for `methods` beside `entry_format=syslog` is an error naming both,
+ because a syslog line carries no request - and a setting that silently does
+ nothing is worse than one that is not offered.
- Measured on an eight core machine, variants interleaved and their order
- reversed between repetitions. 240 `.png` files of 200 kB went from 2.03 to
- 0.44 seconds, which is **4.6 times faster**. 80 `.zip` files of 2 MB, 2.9
- times. 240 `.docx` files of 200 kB, 2.0 times. Two thousand `.txt` files of
- 4 kB, 1.4 times - with files that small the time goes into what a run does
- once rather than into writing them.
+ Every shape still hits the size to the byte, and every line is still a whole
+ entry. `tfg formats log` lists all of it.
- A single file is unchanged whatever its size, and the measurement says so
- rather than the reasoning: at one 20 MB `.png` the two ranges overlap, so no
- difference is claimed. There is nothing to write beside a single file.
+- **JPEG XL, the twenty fourth format.** One frame, 8 bit, RGB.
+ `tfg generate --format jxl --size 300kb` writes a JPEG XL picture in the
+ container the format defines for it. `width`, `height` and `quality` can be
+ set, and the picture goes up to 40 megapixels, so Full HD and 4K are both in
+ reach. Left alone, the picture is the largest of a fixed set that fits the
+ size asked for, up to 640x480 - the same as JPG and AVIF, so the picture
+ formats answer the same request with the same sized picture.
- The gain follows the number of cores you have and the kind of file. Work the
- processor does - drawing a picture, compressing an archive - scales best. A
- run held up by the disk gains less. A handful of files was already quick and
- is unaffected.
+ **Every size from its minimum of 147 B upwards is reachable, with no gaps.**
+ The padding travels in a `free` box, which is the box the container sets aside
+ for space that means nothing, and it takes any length at all.
- **One thing changes if you stop a run part way.** Ctrl+C used to leave behind
- the files finished so far, which were always a consecutive run of them.
- Several threads means one file can be cut off while a later one is already
- finished, so what survives can have a gap in it. The manifest names exactly
- what is on the disk either way, which is what `verify` and `cleanup` work
- from, so neither is affected.
+ The second format here whose pixels are coded by somebody else's encoder. It
+ is pinned, so raising it is a breaking change like any other, and it is pure
+ Go: no C compiler, no shared library and no socket. The files were read back
+ by two independent decoders, one of them libjxl, and both refuse a file that
+ has been truncated or corrupted.
- The progress bar counts the whole run rather than one file at a time, so its
- file counter can move by more than one between redraws.
+- **AVIF, the twenty third format.** One frame, 8 bit, 4:2:0.
+ `tfg generate --format avif --size 300kb` writes an AV1 picture in an ISO base
+ media container. `width`, `height` and `quality` can be set, and the picture
+ goes up to 40 megapixels, so Full HD and 4K are both in reach. Left alone, the
+ picture is the largest of a fixed set that fits the size asked for, up to
+ 640x480 - the same as JPG.
-- **Producing `.png` and `.gif` files is about twice as cheap.** Working out
- what a file will contain used to draw the whole picture and compress it, only
- to throw the result away and do it again when the file was actually written.
- It now does that once.
+ **Every size from its minimum of 311 B upwards is reachable, with no gaps.**
+ The padding travels in a `free` box, which is the box the format sets aside
+ for space that means nothing, and it takes any length at all.
- Measured on 300 files of 200 kB: `.png` takes **2.0 times less processor time
- and 1.8 times less wall clock**. For `.gif`, 1.8 and 1.6.
+ This is the first format here whose pixels are coded by somebody else's
+ encoder rather than by code in this repository. AV1 is too large to write by
+ hand for one format - the coefficient tables alone in the nearest
+ implementation are fourteen times the size of this project's whole WebP
+ encoder. The encoder is pinned, so raising it is a breaking change like any
+ other, and it is pure Go: no C compiler, no shared library and no socket.
- **The files are byte for byte identical.** This changes only how the work is
- ordered, and it was checked that way - across sizes either side of every step
- in the picture ladder, for several seeds, with the label on and off.
+- **WEBP, the twenty second format.** Lossless, one frame, no alpha.
+ `tfg generate --format webp --size 300kb` writes a picture worth 300 kB rather
+ than a thumbnail followed by filler, because the encoder measures out three
+ bytes a pixel and the size is therefore arithmetic - the same shape as BMP and
+ TIFF. `width` and `height` can be set, and naming one lets the other be worked
+ out from the size. The smallest WEBP this produces is 148 B.
- A preview (`--dry-run`) of a large run gets the bigger share of this, since
- previewing was almost entirely the work now removed.
+ **Every size from that minimum upwards is reachable, with no gaps.** No other
+ format here manages that. A WebP is made of RIFF chunks and a chunk always
+ costs an even number of bytes, so the padding is in two parts: a private chunk
+ for the bulk, and up to seven bytes after it for the rest.
- `.jpg` is unchanged and cannot get the same treatment: it writes its padding
- in front of the picture, so it has to know how large the picture is before it
- starts.
+ There is no lossy variant and no `quality`. Lossy WebP is VP8, which is a
+ different codec rather than a setting, and `tfg formats webp` says what this
+ build writes rather than implying more.
-- **A recipe is parsed once instead of twice.** Reading a recipe checked it
- for a stray second document and then handed the whole file to the decoder,
- which parsed it again from scratch. The decoder now works from what was
- already parsed.
+- **`frames` on GIF**, from 1 to 60, default 3. How many frames the animation
+ has. Set it to 1 for a still picture.
+
+- **TIFF, the twenty first format.** Uncompressed, RGB, one page, little-endian.
+ `tfg generate --format tiff --size 300kb` writes a picture worth 300 kB rather
+ than a thumbnail followed by filler, because TIFF stores its pixels
+ uncompressed and the size is arithmetic - the same shape as BMP. `width` and
+ `height` can be set, and naming one lets the other be worked out from the
+ size. The smallest TIFF this produces is 183 B.
+
+### Security
+
+- **On Windows, the desktop window loads the library it uses for dark menus from
+ the system directory rather than by name.** Asking for `uxtheme.dll` by name
+ goes through the standard Windows search order, and the directory the program
+ was started from comes before the system one in that order - so a file of that
+ name left beside a downloaded `tfg-gui.exe` would have been loaded into the
+ program and run.
+
+ Nothing about the window changes. The menu is still dark, which was checked by
+ asking Windows on a real machine rather than by reading the code.
+
+- **The password of a locked archive is no longer repeated in the recorded
+ command line.** It appeared twice in the manifest: under the file's own
+ `properties`, where it is written on purpose because a locked fixture nobody
+ can open checks nothing, and again inside `run.command`, which records the
+ whole command line as typed.
+
+ The second one was a side effect. `run.command` is the line people copy - into
+ a bug report, into a README, into a commit beside a set of fixtures - and it
+ reads like metadata rather than like fixture data, so it was not treated with
+ the same care. It now reads `--set password=***`. The deliberate copy is
+ untouched, so nothing that opens these archives changes.
+
+ **If you compare `run.command` between runs, that string is different now.**
+
+ A manifest that carries a password is also written `0600` rather than `0644`,
+ so it is readable by its owner rather than by every account on the machine.
+ Every other manifest, and every generated file, keeps the mode it had - this
+ tool exists to produce files somebody else's CI will read. Windows has no
+ permission bits, so nothing changes there.
+
+- **Building the program yourself now needs the build tag, and says so if you
+ leave it out.** The AVIF encoder has an assembly path that reads past the end
+ of a buffer and takes the process down on some picture sizes. Every workflow
+ here has passed the tag that turns it off since the encoder arrived - and
+ none of the three ways `README.md` offered for building it yourself did, so
+ `go install`, a distribution package and a build from a checkout all got the
+ path this project describes as reading outside its buffer.
+
+ A build without the tag now stops at the compiler with a message naming it,
+ rather than producing a binary that works until it meets the wrong picture
+ size. Install with:
+
+ ```
+ go install -tags noasm github.com/donislawdev/TestingFilesGenerator/cmd/tfg@latest
+ ```
+
+ The files the program produces are the same either way, so nothing you have
+ generated changes.
+
+- **A recipe that nests lists deeply is refused before it is read, whichever way
+ it is written.** The check that already refused deeply nested brackets counted
+ only brackets, and the same nesting written as `- - - - x` costs two bytes a
+ level and carries none.
+
+ Measured against the previous build. A 500 kB recipe of nothing but dashes ran
+ for 88 seconds and ended in `fatal error: out of memory` with a page of Go
+ internals on standard error, leaving with the exit code a mistyped flag gets.
+ At 1 MB - the largest recipe the size limit allows - it ran for 70 seconds and
+ said nothing at all. The same file is now refused in under a second, with a
+ message naming the depth and the limit.
+
+ The limit counts how deeply lists and mappings nest, in either style, and it
+ is 32. An ordinary recipe reaches one, and an archive declaring what it
+ contains reaches two.
+
+- **A file is never written under a name something else already holds.** Every
+ file this tool writes goes to a temporary name first and is renamed into
+ place. Three of those temporary names were created in a way that follows a
+ link, so a link left at one of them by somebody else sent the bytes wherever
+ it pointed - outside the directory you gave - and the run still reported
+ success.
+
+ Reproduced against the previous build. A link at the manifest's temporary name
+ put the manifest onto a file outside the output directory and exited 0, after
+ which `verify` called that run a match and `cleanup` reported it removed. The
+ same shape made `recipe fmt -w` write your recipe onto somebody else's file
+ and leave your recipe itself as a link. On Windows none of this needs a
+ privilege, because a hard link is enough.
+
+ Every one of those names is now claimed rather than created, and a name
+ something else holds is a refusal that says which name and what to do.
+ Pointing `--out` at a directory reached through a link keeps working, which is
+ the setup this was measured against.
+
+ **What changes for an ordinary run: nothing.** The one case you can meet
+ without somebody working against you is a leftover `.tfg-writing` file from a
+ run that was killed part way through. That used to be written over in silence.
+ It is now a refusal naming the file, so remove it and run again.
+
+### Changed
+
+- **Two commands now refuse a name they cannot answer about, instead of
+ quietly answering about a different one.**
+
+ ```
+ tfg formats png svg described png and said nothing about svg, ending with 0
+ tfg preset list some-name printed the whole list and ignored the name, ending with 0
+ ```
+
+ Both now end with 2 and name the word they could not use. A script that
+ asked about the wrong thing was getting a confident answer about something
+ else, which is worse than being told no.
+
+ `tfg preset list` takes no name at all, so anything after it was always a
+ mistake - most likely somebody reaching for `tfg preset show`.
+ The refusal says so.
+
+ Nothing changes when you pass the right number of names, and no generated
+ file moves a byte.
+
+- **A spreadsheet can now be built wider than a spreadsheet can open.** The
+ `columns` setting of `xlsx` used to stop at 64. It now reaches 32768, which is
+ the ceiling `csv` already had.
+
+ The number matters because of where a reader stops. Excel and LibreOffice
+ Calc both hold 16384 columns. Measured with Calc: a sheet of 16384 columns
+ opens whole, and a sheet of 16385 opens as 16384 - the last column is dropped
+ and nothing is said about it. At a ceiling of 64 there was no way to build a
+ file that asks a spreadsheet about its own limit, which is the kind of file
+ this tool exists to produce.
+
+ `rows` times `columns` still cannot pass 2 million cells, so a sheet 16385
+ columns wide holds up to 122 rows. Nothing about a sheet of 64 columns or
+ fewer changes, and the default is still one column.
+
+- **Byte counts are grouped in threes.** A total used to print as
+ `2516582400 B`. It now prints as `2 516 582 400 B`, in every message that
+ names a number of bytes - `tfg formats`, the summary a run prints, what a
+ preset says its budget is, and what `tfg validate` reports.
+
+ Grouped with a space rather than a comma, because a comma is a thousands mark
+ in some countries and a decimal point in others, and this tool is read in
+ both.
+
+ Machine output is untouched. `--json` and the manifest carry numbers rather
+ than sentences, so nothing that parses those sees any of this. If you have a
+ script reading a byte count out of the human output, it needs to take the
+ spaces out.
+
+- **Notes are reported once per thing they say, not once per file.** A run of
+ 25 000 one-byte text files used to print 25 001 `note:` lines, every one of
+ them the same sentence about the label not fitting. It now prints one, with
+ the count and the first three names:
+
+ ```
+ note: 25000 files: The label needs 32 B and the file is 1 B, so this file
+ carries no label. Its name and the manifest still identify it. Named:
+ files_0001.txt, files_0002.txt, files_0003.txt. 24997 files not named here.
+ ```
+
+ A note about a single file still leads with that file's name, unchanged.
+
+ The reason this matters beyond tidiness: a run whose manifest will be too big
+ for this build to read back warns you first, in a line that looked exactly
+ like the 25 000 that followed it. That warning is the only thing standing
+ between you and a directory that `verify` and `cleanup` can never read.
+
+ **Nothing in the manifest changes.** Every entry still carries its own note,
+ where a machine reads it and nothing scrolls.
+
+- **Building from source now needs Go 1.27.0.** It used to say 1.26.5, and that
+ sentence was true of compiling and false of the product. Go 1.27 changed
+ `compress/flate`, so a copy built on 1.26 answers the same version number,
+ writes that number into every manifest, and produces different bytes for PNG,
+ DOCX and TAR.GZ than the release of that name. The compiler now refuses that
+ build instead of producing one that quietly disagrees with everybody else's.
+
+ If you are pinned to Go 1.26 you can no longer build from source. The
+ downloadable binaries are unaffected, and so is `go install`.
+
+ A manifest already records which Go produced it, under `tool.go`, so two runs
+ that disagree can be told apart after the fact as well.
+
+- **The `encryption` setting now says that a locked archive is a fixture rather
+ than protection.** Nothing about the files changes - the sentence `tfg formats
+ zip` prints, and the one the window shows beside the field, gained the two
+ facts that were missing: the key is worked out from the run seed, so the same
+ recipe gives the same archive on every machine, and the password is written
+ into the manifest beside the file.
+
+ Both are deliberate and both are what makes these archives useful for testing
+ a reader. `aes-256` means something else everywhere else it is written, which
+ is why it is now said out loud. `README.md` says it too.
+
+- **Files are written over several threads, so a run of many files is several
+ times faster.** They used to be written one after another.
+
+ Nothing about what you get changes. The files are byte for byte identical,
+ the manifest lists them in the same order, `verify` and `cleanup` behave
+ exactly as before, and every refusal says what it said.
+
+ Measured on an eight core machine, variants interleaved and their order
+ reversed between repetitions. 240 `.png` files of 200 kB went from 2.03 to
+ 0.44 seconds, which is **4.6 times faster**. 80 `.zip` files of 2 MB, 2.9
+ times. 240 `.docx` files of 200 kB, 2.0 times. Two thousand `.txt` files of
+ 4 kB, 1.4 times - with files that small the time goes into what a run does
+ once rather than into writing them.
+
+ A single file is unchanged whatever its size, and the measurement says so
+ rather than the reasoning: at one 20 MB `.png` the two ranges overlap, so no
+ difference is claimed. There is nothing to write beside a single file.
+
+ The gain follows the number of cores you have and the kind of file. Work the
+ processor does - drawing a picture, compressing an archive - scales best. A
+ run held up by the disk gains less. A handful of files was already quick and
+ is unaffected.
+
+ **One thing changes if you stop a run part way.** Ctrl+C used to leave behind
+ the files finished so far, which were always a consecutive run of them.
+ Several threads means one file can be cut off while a later one is already
+ finished, so what survives can have a gap in it. The manifest names exactly
+ what is on the disk either way, which is what `verify` and `cleanup` work
+ from, so neither is affected.
+
+ The progress bar counts the whole run rather than one file at a time, so its
+ file counter can move by more than one between redraws.
+
+- **Producing `.png` and `.gif` files is about twice as cheap.** Working out
+ what a file will contain used to draw the whole picture and compress it, only
+ to throw the result away and do it again when the file was actually written.
+ It now does that once.
+
+ Measured on 300 files of 200 kB: `.png` takes **2.0 times less processor time
+ and 1.8 times less wall clock**. For `.gif`, 1.8 and 1.6.
+
+ **The files are byte for byte identical.** This changes only how the work is
+ ordered, and it was checked that way - across sizes either side of every step
+ in the picture ladder, for several seeds, with the label on and off.
+
+ A preview (`--dry-run`) of a large run gets the bigger share of this, since
+ previewing was almost entirely the work now removed.
+
+ `.jpg` is unchanged and cannot get the same treatment: it writes its padding
+ in front of the picture, so it has to know how large the picture is before it
+ starts.
+
+- **A recipe is parsed once instead of twice.** Reading a recipe checked it
+ for a stray second document and then handed the whole file to the decoder,
+ which parsed it again from scratch. The decoder now works from what was
+ already parsed.
Measured on the largest recipe the size limit allows, 900 kB and 20 000
targets: `validate` went from 839 ms to 754 ms. An ordinary recipe of a few
@@ -591,6 +974,18 @@ because it turns other people's test suites red.
somebody else put there, and `tfg cleanup` will not remove it, because it
removes only what a manifest lists.
+ Taking the name at the start has a second effect, on a run nothing else is
+ competing with. A run that filled the disk part way through used to lose its
+ manifest as well, because the manifest asked for its name after the last file
+ and by then there was no room left for it. Such a run ended with exit code
+ `5`, printed the files it had not produced, and said that clearing the
+ directory was a job by hand. The name is now taken before the first file, so
+ the manifest is written whatever happens to the disk afterwards, `tfg cleanup`
+ can remove what the run left, and the run ends with the partial exit code `8`
+ instead. Measured in a container with room for twelve names: 0.2.0 and
+ 0.3.0-rc1 both ended `5` and wrote no manifest, and this release ends `8` and
+ writes one.
+
The cost is worth stating plainly: two runs can no longer fill one directory
at the same time, even when the files they write have different names. For
every run that does not set `output.manifest` that was already true.
@@ -668,395 +1063,6 @@ because it turns other people's test suites red.
- **Every run used to pause twice to tidy memory**, however small it was. It
pauses once. Nothing about what a run produces changes.
-## [0.3.0-rc1] - 2026-09-03
-
-### Breaking
-
-- **A generated `.csv` quotes only the fields that need it, so its bytes are
- different.** Sizes are unchanged. Every size that worked before still works,
- every reader that took these files still takes them, and the file is still
- RFC 4180.
-
- The description column used to be quoted on every row. It is quoted now only
- when it carries the separator, which is the one thing that makes a quote
- necessary. The description is three to seven words and drops a separator
- every third one, so a short one carries none - measured on a 4 kB table,
- **9 of its 44 rows** lost their quotes.
-
- This arrives as a new setting, `quote_style`, which takes `minimal`, `all` or
- `none`:
-
- - `minimal` is the new default and is what a spreadsheet writes.
- - `all` wraps every field on every row, the header included.
- - `none` wraps nothing. It also stops the description carrying the separator,
- because an unquoted field cannot hold one without ending early - so this
- value changes what the file says and not only how it is punctuated.
-
- **The smallest `.csv` is 115 B rather than 117**, because the shortest row
- has an empty description and an empty field needs no quotes. With
- `quote_style=all` the smallest is 139 B. `tfg formats` prints the current
- numbers.
-
- **A suite pinning `.csv` hashes will go red once and then stay green.** There
- is no switch back to the old bytes: they were not any of the three styles RFC
- 4180 describes, and carrying a fourth name for them forever costs more than
- the one red run.
-
-- **Six formats have different bytes, because the tool is built with Go 1.27
- now.** Sizes are unchanged. Every size that worked before still works, every
- reader that took these files still takes them, and the same sizes are
- reachable. What changed is the compressed data inside them.
-
- Affected: `targz`, `png`, `docx`, `xlsx`, `pptx`, `ico` when it holds a png,
- and `zip` when you ask for compression. `zip` left alone is untouched,
- because its default stores rather than compresses. The other seventeen
- formats are byte for byte what they were.
-
- Go 1.27 changed `compress/flate`, which is what all of those run through.
- Below the default compression level the change is only how a stream is
- closed, and above it the compressor itself behaves differently.
-
- Two minimums moved with it. The smallest `png` is **74 B** rather than 73,
- and sizes 75 to 82 and 85 are the ones it cannot produce. The smallest
- `targz` is **1049 B** rather than 1052, the next size up is 1051, and 1050 is
- the one it cannot produce. `tfg formats` prints the current numbers.
-
- **A suite pinning hashes for those formats will go red once and then stay
- green.** There is no switch back: staying on the old compiler was not a
- choice this tool can offer, since the compiler comes from whoever builds it.
-
-- **`.tar.gz` could not be produced at all under Go 1.27 until this release.**
- Every size was refused with an error saying the generator produced three
- bytes fewer than planned. The size of a `.tar.gz` is worked out rather than
- measured - compressing twice to learn a length would make a preview cost what
- the run costs - and that arithmetic carried a number that turned out to
- describe one release of Go.
-
- It measures that number now, at first use, and checks its own answer before
- trusting it. A later Go release can move these bytes again, but it can no
- longer stop the format from being written.
-
-- **A generated `.tar.gz` has different bytes, because a lot of them could
- not be opened by a Go program.** Sizes are unchanged, every size that
- worked before still works, and every reader that took these files still
- takes them. What moved is where the padding sits inside the header.
-
- The padding used to ride in the gzip header comment. Go's own
- `compress/gzip` reads that field into a fixed buffer and refuses a comment
- of 512 bytes or more, so **4134 of the 11 260 reachable sizes produced an
- archive no Go program could open** - and the message it gives,
- `gzip: invalid header`, reads like a corrupt file rather than like a field
- the reader will not take. 7-Zip, GNU tar, bsdtar, Python and node all took
- those files without a word, which is why it was not noticed sooner.
-
- The padding now rides in the gzip extra field, which Go reads to the end
- of. After the change: 11 260 sizes reachable, none unreachable, none
- unreadable.
-
- **There is no way back to the old bytes**, and that is the difference
- between this and the other two entries here. The old bytes are the ones a
- Go program cannot read, so keeping a switch for them would be keeping a
- switch for the fault. A suite pinning `.tar.gz` hashes will go red once
- and then stay green.
-
-- **A generated log now advances through time, so its bytes are different.**
- Every entry used to carry the same instant. Ten thousand requests all landing
- at one moment is not a log anybody can test a time window, a rate alert or a
- rotation against, and it was obvious the moment you looked at a file.
-
- Entries are now one second apart by default, and `rate` sets how many arrive
- a second. The bytes of every log change, so a suite pinning their hashes will
- go red.
-
- **The way back is `--set timestamps=fixed`**, or `timestamps: fixed` on a
- target in a recipe. That holds the clock still and writes the same bytes this
- tool wrote before, to the byte - there is a pinned hash proving it.
-
-- **A generated GIF now moves, so its bytes are different.** A GIF is the one
- picture format here that can hold more than one frame, and a still one told
- you nothing about how the system under test treats an animation - whether it
- keeps it, flattens it to the first frame, or re-encodes it. Every GIF now
- carries a marker that travels across the picture in three frames, and the
- manifest says `animated` and `frame_count` for each file.
-
- Two things change with it. The smallest GIF this tool will write goes from
- 41 B to 114 B, because the number a format announces as its minimum has to be
- a number a plain run accepts, and a plain run animates. And the bytes of every
- GIF change, so a suite pinning their hashes will go red.
-
- **The way back is `--set frames=1`**, or `frames: 1` on a target in a recipe.
- That takes the plain encoder and writes the same bytes this tool wrote before,
- to the byte - there is a pinned hash proving it.
-
-- **A setting value is spelled the way the format declares it.** `--set
- page_size=A4` and `--set directory_entries=TRUE` used to be accepted and now
- refuse with exit 4, naming the setting and the value. Write `a4` and `true`.
-
- Nothing else changes: the bytes of every file are what they were, and a
- recipe writing `header: true` is unaffected, because a YAML boolean arrives
- as `true` either way. Only a value quoted into another case is refused.
-
- It is a breaking change for a fourth of a reason and a fix for the rest. A
- value the declaration did not contain used to pass the check and land on the
- format, which then did one of four things with it: refuse in its own words,
- understand it anyway, **quietly ignore it and produce the default file**, or
- read it as something else entirely. `--set entry_owner=USER` on a `targz`
- wrote an archive owned by nobody and reported success. Now nothing gets that
- far.
-
-### Added
-
-- **`csv` takes `columns`, so a table can be as narrow or as wide as the thing
- you are testing.** Two to 32768, and six by default - which is the six
- columns this tool has always written, so a `.csv` you already generate does
- not change by a byte.
-
- Fewer than six drops them from the middle: `id` stays first and
- `description` stays last, because that is the field stretched to reach the
- exact size you asked for. More than six adds `field_7`, `field_8` and so on
- in front of the description.
-
- **Above 16384 columns a spreadsheet quietly keeps the first 16384 and drops
- the rest.** Measured with LibreOffice Calc: 16384 comes back whole, 16385
- comes back with one column missing and no warning anywhere. The ceiling here
- is deliberately past that, so you can build the set either side of the line
- rather than only the last file that survives it.
-
- The smallest file moves with the setting, the way it already does for row
- endings and quoting - 36 B at two columns, 115 B at six, 5017 B at 256.
- `tfg formats csv` prints what a given table needs.
-
-- **A `targz` manifest says what its entries claim about themselves.** Two new
- keys on the file entry, `entry_mode` and `entry_owner`, written every run
- rather than only when you ask for them, so a harness never has to read a
- missing key as "nobody owns this". The manifest schema version is unchanged.
-
-- **A CSV can be written in the dialect you were handed.** `--set
- delimiter=semicolon`, `--set line_ending=crlf` and `--set header=false` on a
- `csv`, separately or together. Separators are named rather than typed, so
- `tab` and `pipe` need no escaping: the four are `comma`, `semicolon`, `tab`
- and `pipe`.
-
- These are the three ways a real CSV differs before its contents do. A
- European spreadsheet exports with semicolons, anything written on Windows
- ends its rows with CRLF, and a table dumped straight out of a database has no
- header. All three are CSV and all three break a reader that assumed the other
- thing.
-
- The description column keeps carrying the separator, so a semicolon file
- still exercises quoted fields rather than quietly testing less than a comma
- one does.
-
- Two things worth knowing. The smallest file changes with the dialect, because
- a CRLF row is a byte longer and a header is a whole line - the tool tells you
- the floor for the settings you gave it. And the manifest records the
- separator as the character that is in the file, where the recipe names it as
- a word.
-
- The defaults are `comma`, `lf` and a header, which is what this tool has
- always written, so **no existing file changes by a byte**.
-
-- **A log can be made quiet, or full of errors.** `--set level_mix=errors` on
- a `log`, with `realistic`, `quiet`, `errors` and `debug` to choose from. It
- decides which severities appear, the way `status_mix` already decides which
- response codes do.
-
- Only the `plain` and `json-lines` entry formats carry a severity at all. Ask
- for a mix beside one of the other four and the tool says so and stops,
- naming both settings, rather than accepting a setting that would do nothing.
-
- One thing worth knowing before you pick `quiet`: it draws only `INFO`, which
- is a shorter word than `ERROR`, so the smallest log it can write is one byte
- smaller than the other mixes. The tool tells you the floor for the settings
- you gave it.
-
- The default is `realistic`, the mix these logs have always had, so **no
- existing file changes by a byte**.
-
-- **An archive can compress what it holds.** `--set compression=best` on a
- `zip` or a `targz`, with `none`, `fast`, `default` and `best` to choose from.
-
- The archive still comes out **exactly the size you asked for**. What changes
- is how much of it is your files and how much is padding: at `best` a
- megabyte archive holding four 32 KB text files carries the same four files
- deflated, and the padding entry grows to make up the difference. A reader
- sees real deflated entries, which is what a tool under test has to cope with.
-
- The default is `none`, which is what archives from this tool have always
- been, so **no existing file changes by a byte**.
-
- Two combinations are refused rather than half-supported, and the message
- says which two settings to choose between. Compression with a size taken
- **from the contents**: the archive's length would then be whatever the
- contents compress to, which is only knowable by compressing them, and that
- would make a preview cost as much as the run. Compression with a
- **password**: a locked entry has to state its length before its data is
- written, so a compressed one would have to be held in memory whole.
-
- Compressing costs time at write, not at preview. A 10 MB archive takes about
- 25 ms at `fast` and 140 ms at `default`, against 8 ms stored, and a `.tar.gz`
- pays that twice because gzip compresses the whole stream and the size has to
- be measured before it can be hit.
-
-- **An archive can hold its files in directories.** `--set depth=3` puts every
- file three levels down, and `--set directory_entries=true` also makes the
- archive list the directories themselves. Both work on `zip` and on `targz`.
-
- Two settings rather than one, because they are two questions. Depth is about
- the paths inside. Directory entries are about whether the archive names the
- directories at all - and extractors differ there: some create a directory
- when they meet a path that needs one, and some create only what the archive
- names. An archive is the one format where you can test both.
-
- The default is flat, which is what archives from this tool have always been,
- so **no existing file changes by a byte**. Asking for `directory_entries`
- without a depth is refused rather than quietly ignored: a flat archive has no
- directories to name, and the message says so and names both settings.
-
- Depth goes up to 50. The limit is measured rather than picked: a `.tar.gz`
- writes USTAR headers, which carry a path in a 155 byte prefix and a 100 byte
- name split on a slash, and past a certain length no split works. Directories
- cost 512 bytes each in a `.tar.gz` and about 76 plus the path in a `.zip`.
- The size you order is still the size you get, to the byte.
-
- The padding entry stays at the top of the archive rather than moving into the
- directories, so you can always tell it apart from the files you asked for.
-
-- **A zip can be locked with ZipCrypto, the old scheme.** `--set encryption=zipcrypto`.
-
- It is here for what it does to a reader rather than for what it protects. Measured: .NET's own `ZipFile` opens one of these, reports the entry at its true length, hands back a stream and fills it with the ENCRYPTED bytes - and never says the entry was encrypted at all. An application built on that library processes noise and calls it data. AES fails loudly in the same library, which is the safer defect and the less interesting one.
-
- So this is the fixture for finding out whether something in a pipeline waves an encrypted archive through.
-
- **It is not protection and it is not offered as any.** ZipCrypto has been broken for decades. Use `aes-256` when the point is that the contents are hard to read.
-
-- **A zip can be locked with a password.**
-
- tfg generate --format zip --size 30kb --set entries=3 \
- --set password=Secret123 --set encryption=aes-256
-
- writes an archive of exactly 30720 B that 7-Zip opens with that password
- and refuses without it. The methods are `aes-128`, `aes-192` and
- `aes-256`.
-
- **The password goes into the manifest in plain text.** A locked fixture
- nobody can open is worth nothing, so the manifest records it exactly as
- you typed it - that is the point rather than a leak. Do not use a password
- you use anywhere else.
-
- Both settings are needed together. A password with `encryption=none`, or
- an encryption with no password, is refused rather than guessed at, and the
- refusal names both of them.
-
- **Limits worth knowing before you build a fixture.** Some readers cannot
- open AES archives at all - .NET's own `ZipFile` lists the entries and then
- fails on reading one. Nothing in this build writes the older ZipCrypto
- scheme yet, so an archive meant for a reader that only speaks that is not
- something this can make. And `tar.gz` cannot be locked at all - neither
- tar nor gzip has any encryption in it, and asking for one there is refused
- with that reason rather than ignored.
-
-- **A tar.gz can say what permissions its files have and who owns them.**
- `--set entry_mode=755` and `--set entry_owner=root`. The modes are the
- ones chmod takes, from `000` through `777`, and the owners are `unset`
- (the default, and what this tool has always written), `root` and `user`.
-
- The useful cases are the ones nobody makes by accident: `000` is a file
- nothing can read after unpacking, `777` is one a scanner should have
- something to say about, and an archive claiming root owns everything is
- what a careless extractor turns into a privilege problem.
-
- It changes no bytes unless you ask for it, and the size of the archive is
- the same either way.
-
-- **A log can now be six shapes rather than one, and seven settings shape it.**
- `tfg generate --format log --set entry_format=nginx` writes an nginx access
- log. The others are `apache-combined` (the default, and what this format has
- always written), `apache-common`, `syslog`, `plain` and `json-lines`.
-
- Every template was taken from a real file rather than from a specification
- remembered: a real nginx and a real Apache, and rsyslog on a real machine. Two
- of them would have been wrong otherwise. An nginx line carries one more
- quoted field than "combined" does, and Apache's own default is `common`, with
- no referrer and no agent at all.
-
- The rest of the settings: `timestamps` and `rate` for the clock, `methods` for
- which verbs appear, `status_mix` for which response codes, `ip_version` to put
- IPv6 addresses in front of a reader that may not expect them, and
- `line_ending` for a log written by a Windows service.
-
- **A setting that could not do anything is refused rather than ignored.**
- Asking for `methods` beside `entry_format=syslog` is an error naming both,
- because a syslog line carries no request - and a setting that silently does
- nothing is worse than one that is not offered.
-
- Every shape still hits the size to the byte, and every line is still a whole
- entry. `tfg formats log` lists all of it.
-
-- **JPEG XL, the twenty fourth format.** One frame, 8 bit, RGB.
- `tfg generate --format jxl --size 300kb` writes a JPEG XL picture in the
- container the format defines for it. `width`, `height` and `quality` can be
- set, and the picture goes up to 40 megapixels, so Full HD and 4K are both in
- reach. Left alone, the picture is the largest of a fixed set that fits the
- size asked for, up to 640x480 - the same as JPG and AVIF, so the picture
- formats answer the same request with the same sized picture.
-
- **Every size from its minimum of 147 B upwards is reachable, with no gaps.**
- The padding travels in a `free` box, which is the box the container sets aside
- for space that means nothing, and it takes any length at all.
-
- The second format here whose pixels are coded by somebody else's encoder. It
- is pinned, so raising it is a breaking change like any other, and it is pure
- Go: no C compiler, no shared library and no socket. The files were read back
- by two independent decoders, one of them libjxl, and both refuse a file that
- has been truncated or corrupted.
-
-- **AVIF, the twenty third format.** One frame, 8 bit, 4:2:0.
- `tfg generate --format avif --size 300kb` writes an AV1 picture in an ISO base
- media container. `width`, `height` and `quality` can be set, and the picture
- goes up to 40 megapixels, so Full HD and 4K are both in reach. Left alone, the
- picture is the largest of a fixed set that fits the size asked for, up to
- 640x480 - the same as JPG.
-
- **Every size from its minimum of 311 B upwards is reachable, with no gaps.**
- The padding travels in a `free` box, which is the box the format sets aside
- for space that means nothing, and it takes any length at all.
-
- This is the first format here whose pixels are coded by somebody else's
- encoder rather than by code in this repository. AV1 is too large to write by
- hand for one format - the coefficient tables alone in the nearest
- implementation are fourteen times the size of this project's whole WebP
- encoder. The encoder is pinned, so raising it is a breaking change like any
- other, and it is pure Go: no C compiler, no shared library and no socket.
-
-- **WEBP, the twenty second format.** Lossless, one frame, no alpha.
- `tfg generate --format webp --size 300kb` writes a picture worth 300 kB rather
- than a thumbnail followed by filler, because the encoder measures out three
- bytes a pixel and the size is therefore arithmetic - the same shape as BMP and
- TIFF. `width` and `height` can be set, and naming one lets the other be worked
- out from the size. The smallest WEBP this produces is 148 B.
-
- **Every size from that minimum upwards is reachable, with no gaps.** No other
- format here manages that. A WebP is made of RIFF chunks and a chunk always
- costs an even number of bytes, so the padding is in two parts: a private chunk
- for the bulk, and up to seven bytes after it for the rest.
-
- There is no lossy variant and no `quality`. Lossy WebP is VP8, which is a
- different codec rather than a setting, and `tfg formats webp` says what this
- build writes rather than implying more.
-
-- **`frames` on GIF**, from 1 to 60, default 3. How many frames the animation
- has. Set it to 1 for a still picture.
-
-- **TIFF, the twenty first format.** Uncompressed, RGB, one page, little-endian.
- `tfg generate --format tiff --size 300kb` writes a picture worth 300 kB rather
- than a thumbnail followed by filler, because TIFF stores its pixels
- uncompressed and the size is arithmetic - the same shape as BMP. `width` and
- `height` can be set, and naming one lets the other be worked out from the
- size. The smallest TIFF this produces is 183 B.
-
-### Fixed
-
- **A run of a few files is now weighed against the memory ceiling too.** The
ceiling that stops a run from planning more than it can hold only started
counting once a run asked for sixty four files, so a smaller run had no
@@ -1675,7 +1681,7 @@ because it turns other people's test suites red.
Initial release.
-[Unreleased]: https://github.com/donislawdev/TestingFilesGenerator/compare/v0.3.0-rc1...HEAD
-[0.3.0-rc1]: https://github.com/donislawdev/TestingFilesGenerator/compare/v0.2.0...v0.3.0-rc1
+[Unreleased]: https://github.com/donislawdev/TestingFilesGenerator/compare/v0.3.0...HEAD
+[0.3.0]: https://github.com/donislawdev/TestingFilesGenerator/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/donislawdev/TestingFilesGenerator/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/donislawdev/TestingFilesGenerator/releases/tag/v0.1.0
diff --git a/README.md b/README.md
index b4cd509..f85e991 100644
--- a/README.md
+++ b/README.md
@@ -527,7 +527,7 @@ interrupted. One entry per file:
```json
{
"manifest_version": "1.0",
- "tool": { "name": "testing-files-generator", "version": "0.2.0" },
+ "tool": { "name": "testing-files-generator", "version": "0.3.0" },
"run": {
"id": "run_b359aa8d94",
"seed": 0,
diff --git a/cmd/tfg-gui/rsrc_windows_amd64.syso b/cmd/tfg-gui/rsrc_windows_amd64.syso
index bb0c063..1bba15e 100644
Binary files a/cmd/tfg-gui/rsrc_windows_amd64.syso and b/cmd/tfg-gui/rsrc_windows_amd64.syso differ
diff --git a/cmd/tfg/rsrc_windows_amd64.syso b/cmd/tfg/rsrc_windows_amd64.syso
index 8d2748f..d737cfc 100644
Binary files a/cmd/tfg/rsrc_windows_amd64.syso and b/cmd/tfg/rsrc_windows_amd64.syso differ
diff --git a/cmd/tfg/tfg.rc b/cmd/tfg/tfg.rc
index 4432202..f6b2ee8 100644
--- a/cmd/tfg/tfg.rc
+++ b/cmd/tfg/tfg.rc
@@ -50,12 +50,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "DonislawDev"
VALUE "FileDescription", "Generates test files for QA, from the command line"
- VALUE "FileVersion", "0.3.0-rc1"
+ VALUE "FileVersion", "0.3.0"
VALUE "InternalName", "tfg"
VALUE "LegalCopyright", "Copyright (C) 2026 DonislawDev. GNU GPL version 3."
VALUE "OriginalFilename", "tfg.exe"
VALUE "ProductName", "Testing Files Generator"
- VALUE "ProductVersion", "0.3.0-rc1"
+ VALUE "ProductVersion", "0.3.0"
END
END
BLOCK "VarFileInfo"
diff --git a/internal/guard/testdata/screens/about.png b/internal/guard/testdata/screens/about.png
index ac6da47..18dcc7a 100644
Binary files a/internal/guard/testdata/screens/about.png and b/internal/guard/testdata/screens/about.png differ
diff --git a/internal/guard/testdata/screens/about.xml b/internal/guard/testdata/screens/about.xml
index e08f617..c934644 100644
--- a/internal/guard/testdata/screens/about.xml
+++ b/internal/guard/testdata/screens/about.xml
@@ -30,7 +30,7 @@
The address you followed does not