diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bfe273d..95f8fbfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1373,6 +1373,23 @@ jobs: ogrinfo --version python -c "import shapely; print('shapely', shapely.__version__, 'GEOS', shapely.geos_version_string)" + # Mail is read by mailparser beside Python's email package - the + # parser much of the mail handling written in Node uses, and the one + # that refuses a message of more than 1000 MIME entities + # (docs/EML-2026-10-07.md section 5.1). Pinned, installed outside the + # checkout, and named to the guard through TFG_MAILPARSER. Asked for + # its version, so a package that did not install stops this step, and + # the guard fails if it is named and reads nothing. + # + # Without a committed lockfile, on purpose for now: one in the + # repository puts the whole tree into the dependency graph, and the + # licence gate then blocks two packages of it (review of #173, + # docs/REVIEW-173-2026-10-07.md). Whether CI-only tools belong under + # that gate is the owner's decision, not a fix. + npm install --prefix "$RUNNER_TEMP/mailparser" mailparser@3.9.36 + echo "TFG_MAILPARSER=$RUNNER_TEMP/mailparser/node_modules/mailparser" >> "$GITHUB_ENV" + node -e "console.log('mailparser', require(process.argv[1] + '/package.json').version)" "$RUNNER_TEMP/mailparser/node_modules/mailparser" + # The distribution's Pillow cannot open an AVIF - measured on this # runner, PIL.UnidentifiedImageError on a file the structural checker # had just called sound. Installed for the SAME interpreter the oracle diff --git a/CHANGELOG.md b/CHANGELOG.md index 8043ecaa..6d4786af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,25 @@ because it turns other people's test suites red. refuses a text value of ten million characters. At precision 3 or less a line has fewer points than that, one for each step of longitude the globe has, and asking for more is refused with the number that fits. +- **Mail: the `eml` format.** A mail message (RFC 5322 with MIME) at the + exact size you ask for, with real files of any other format attached in + base64 - `attachments`, `attachment_format` and `attachment_size`, or + `contains` for files that are not all alike. The size is the message's, + about a third more than the files it carries, and the manifest records + both, with the number of MIME entities and the subject and Message-ID a + test can find the message by. `body` gives a text part (the default), an + HTML part, or both with the same words. `text_encoding` keeps the text to + 7bit, or adds a line in Polish and Japanese in quoted-printable or 8bit. + `headers` puts letters outside ASCII into the subject, the names of the + people and the names of attached files, encoded the way RFC 2047 and RFC + 2231 say or raw as RFC 6532 allows. `filename_style` writes those names + four ways, and readers do not agree on two of them: Go's standard library + reads no name from `content-type` and the encoded text itself from + `rfc2047`. `line_endings` gives `crlf` or `lf`. Read by Python's email + package, mailparser and Go's standard library, and checked line by line + against RFC 5322, which none of the three does. The smallest message at + the default settings is 289 B. mailparser refuses a message of more than + 1000 MIME entities, which 999 attached files and the text reach. - **GeoJSON: holes, features with no place, both types of id, and bbox.** `holes` gives every polygon up to 100 000 holes of four points, each running the other way round to its outline as RFC 7946 asks. Holes need 6 or more @@ -194,10 +213,10 @@ because it turns other people's test suites red. ### Changed -- **`empty-and-minimal` now includes the films and GeoJSON.** The preset builds the +- **`empty-and-minimal` now includes the films, GeoJSON and mail.** The preset builds the smallest file of every format this build has, so with `formats` left at - `all` it gains a `webm`, an `mp4` and a `geojson` target, and the recipe - `tfg preset eject empty-and-minimal` prints is three targets longer. A manifest from a run of that preset records the + `all` it gains a `webm`, an `mp4`, a `geojson` and an `eml` target, and the recipe + `tfg preset eject empty-and-minimal` prints is four targets longer. A manifest from a run of that preset records the hash of that recipe, so its `recipe_hash` differs from one written before. No generated file changes, and a run that names its `formats` is untouched. - **An empty text box in the window reads "not set"** instead of "worked out diff --git a/README.md b/README.md index 670dd75f..99ea9f69 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ **Testing Files Generator** is a tool for QA engineers and developers who need real files to test against - an upload form, a parser, anything that takes a file and -has an opinion about it. You pick one of its 29 formats and the size you want, +has an opinion about it. You pick one of its 30 formats and the size you want, and you get **exactly that**: ask for a 10 MB PDF and you get a PDF that a reader will open, at 10 MB to the byte. Every run also leaves a manifest saying **what your system should do with each file**, which is the part other generators leave @@ -23,7 +23,7 @@ needs it finds out it exists. - **Hit an exact size, to the byte** - ask for 10485761 bytes and get exactly that, never a silently rounded file. -- **Write 29 real formats** - a generated PNG opens in an image viewer, a DOCX +- **Write 30 real formats** - a generated PNG opens in an image viewer, a DOCX opens in Word, a ZIP extracts. Not padded zeros with an extension. - **Say what should happen to each file** - the manifest carries an expected outcome, so your test reads the assertion instead of you writing it out. @@ -166,6 +166,7 @@ owns it, at the exact size you asked for: | 📝 **Text and markup** | `txt`, `md`, `csv`, `json`, `xml`, `html`, `log` | | ⚙️ **Configuration** | `yaml`, `toml` | | 🗺️ **Maps** | `geojson` (RFC 7946) | +| ✉️ **Mail** | `eml` (RFC 5322 with MIME, real files attached) | | 🗜️ **Archives** | `zip`, `targz` (`.tar.gz`) | | 🔊 **Audio** | `wav` | | 🎞️ **Video** | `webm`, `mp4` (both AV1, no sound) | @@ -618,6 +619,7 @@ recipe. `tfg formats ` prints the allowed range or list for each: | `txt`, `md`, `xml` | `encoding`, `bom` | | `json` | `formatting` | | `geojson` | `geometry`, `formatting`, `precision`, `altitude`, `vertices`, `holes`, `winding`, `antimeridian`, `unlocated`, `ids`, `bbox` | +| `eml` | `body`, `text_encoding`, `headers`, `line_endings`, `attachments`, `attachment_format`, `attachment_size`, `filename_style` | | `svg` | `width`, `height` | | `html` | `structure` | | `yaml`, `toml` | none in this build - the document is a fixed shape, and a size is the only thing to ask for | diff --git a/internal/engine/engine.go b/internal/engine/engine.go index b083f96c..9df4428f 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -271,7 +271,7 @@ func settleTarget(t *Target, opt Options, seen map[string]bool) (format.Descript // ignoring contains would give an archive with none of the files somebody // listed, reported as a success - the file looks right and the test suite // believes it. - if len(t.Contains) > 0 && !desc.Container { + if len(t.Contains) > 0 && desc.Container == nil { return format.Descriptor{}, &format.NotAContainerError{Format: t.Format, Containers: format.Containers()} } diff --git a/internal/format/all/all.go b/internal/format/all/all.go index cfbbb057..0f7adfb0 100644 --- a/internal/format/all/all.go +++ b/internal/format/all/all.go @@ -12,6 +12,7 @@ import ( _ "github.com/donislawdev/TestingFilesGenerator/internal/format/bmp" _ "github.com/donislawdev/TestingFilesGenerator/internal/format/csvfile" _ "github.com/donislawdev/TestingFilesGenerator/internal/format/docx" + _ "github.com/donislawdev/TestingFilesGenerator/internal/format/eml" _ "github.com/donislawdev/TestingFilesGenerator/internal/format/geojson" _ "github.com/donislawdev/TestingFilesGenerator/internal/format/gif" _ "github.com/donislawdev/TestingFilesGenerator/internal/format/htmlfile" diff --git a/internal/format/archive/archive.go b/internal/format/archive/archive.go index 35d426b9..9dc609f3 100644 --- a/internal/format/archive/archive.go +++ b/internal/format/archive/archive.go @@ -251,8 +251,46 @@ func Axes(names ...string) []format.Property { // is what an archive may not hold, since an archive inside an archive needs a // depth limit that does not exist yet. func Groups(id string, r format.Request) ([]format.Content, error) { + return GroupsThrough(id, r, archiveDoor) +} + +// ArchiveMembers is how an archive names what it holds, for its descriptor. +var ArchiveMembers = format.Members{Count: Entries, Format: EntryFormat, Size: EntrySize} + +// MaxMembers is the most files a container holds, through either door. A +// container that is not an archive declares its own count with this ceiling, +// so the two doors of every container stop at one number. +const MaxMembers = maxEntries + +// DefaultSizeText is how big each file inside is when nothing says, written +// the way a declaration shows it. +const DefaultSizeText = defaultSizeText + +// Door is how a container names its contents, for GroupsThrough. +// +// The rules are one set for every container - both doors at once refused, the +// container never inside itself, one ceiling on both - and what differs is the +// words: which keys, how many files when nothing says, and the noun in the +// sentence that asks for fewer. +type Door struct { + Members format.Members + DefaultCount int + // Fewer is the remedy when more files are asked for than MaxMembers. + Fewer core.Said +} + +// archiveDoor is the door both archives share. +var archiveDoor = Door{ + Members: ArchiveMembers, + DefaultCount: defaultEntries, + Fewer: core.Says("format.AskForEntriesOrFewer", "Ask for %d entries or fewer.", core.A("MaxEntries", maxEntries)), +} + +// GroupsThrough is Groups for a container that names its contents its own way. +func GroupsThrough(id string, r format.Request, door Door) ([]format.Content, error) { + keys := door.Members var stated []string - for _, key := range []string{Entries, EntryFormat, EntrySize} { + for _, key := range []string{keys.Count, keys.Format, keys.Size} { if _, ok := r.Properties[key]; ok { stated = append(stated, key) } @@ -262,7 +300,7 @@ func Groups(id string, r format.Request) ([]format.Content, error) { // which is a legitimate thing to ask for, and it is a different statement // from saying nothing at all. if r.Contains != nil { - return fromContains(id, r, stated) + return fromContains(id, r, stated, door.Fewer) } if r.SizeFromContents { @@ -271,19 +309,19 @@ func Groups(id string, r format.Request) ([]format.Content, error) { return nil, core.Defect(fmt.Errorf("%s: the size was left to the contents and there are none", id)) } - entries, err := intProperty(id, r.Properties, Entries, defaultEntries, 0, maxEntries) + entries, err := intProperty(id, r.Properties, keys.Count, door.DefaultCount, 0, maxEntries) if err != nil { return nil, err } // Sizes in properties use the same syntax as --size. Anything else would // mean entry_size=200kb failing while size=200kb works, which nobody would // predict. - entrySize, err := sizeProperty(id, r.Properties, EntrySize, defaultSize) + entrySize, err := sizeProperty(id, r.Properties, keys.Size, defaultSize) if err != nil { return nil, err } entryFmt := DefaultFormat - if v, ok := r.Properties[EntryFormat]; ok && v != "" { + if v, ok := r.Properties[keys.Format]; ok && v != "" { entryFmt = v } if entryFmt == id { @@ -297,7 +335,7 @@ func Groups(id string, r format.Request) ([]format.Content, error) { // Split out of Groups so the branching stays under the ceiling the shape gates // hold, and because it is one subject: everything here is about a contains list // and nothing about it reads a property. -func fromContains(id string, r format.Request, stated []string) ([]format.Content, error) { +func fromContains(id string, r format.Request, stated []string, fewer core.Said) ([]format.Content, error) { if len(stated) > 0 { return nil, &format.ContentsConflictError{Format: id, Keys: stated} } @@ -319,7 +357,7 @@ func fromContains(id string, r format.Request, stated []string) ([]format.Conten // to the exit code: a plain error here would have landed on 1 while // the same request through entries lands on 4, which is the same // disagreement one level further down. - return nil, tooMany(id, "contains", asked) + return nil, tooMany(id, "contains", asked, fewer) } return r.Contains, nil } @@ -328,13 +366,13 @@ func fromContains(id string, r format.Request, stated []string) ([]format.Conten // // TestBothWaysOfAskingForEntriesShareOneCeiling compares the reason the two // produce, and before this it compared two sentences somebody had typed twice. -func tooMany(id, key string, asked int) *format.PropertyValueError { +func tooMany(id, key string, asked int, fewer core.Said) *format.PropertyValueError { return &format.PropertyValueError{ Format: id, Key: key, Value: strconv.Itoa(asked), Reason: core.Says("format.ItTakesAWholeNumberFrom", "it takes a whole number from 0 to %d", core.A("MaxEntries", maxEntries)), - Remedy: core.Says("format.AskForEntriesOrFewer", "Ask for %d entries or fewer.", core.A("MaxEntries", maxEntries)), + Remedy: fewer, } } @@ -365,3 +403,18 @@ func intProperty(id string, props map[string]string, key string, fallback, min, } return n, nil } + +// ContentSummary is what a container holds, in the manifest, so a test can +// assert on it without opening the file. One copy for every container, so the +// key names a test reads cannot differ between them. +func ContentSummary(groups []format.Content) []map[string]any { + out := make([]map[string]any, 0, len(groups)) + for _, g := range groups { + out = append(out, map[string]any{ + "format": g.Format, + "count": g.Count, + "bytes": g.Bytes, + }) + } + return out +} diff --git a/internal/format/avif/avif.go b/internal/format/avif/avif.go index 6e00a82a..dc1230e1 100644 --- a/internal/format/avif/avif.go +++ b/internal/format/avif/avif.go @@ -102,6 +102,7 @@ func init() { ID: "avif", Name: "AV1 Image File Format", Extension: ".avif", + MediaType: "image/avif", // IANA, Alliance for Open Media Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/bmp/bmp.go b/internal/format/bmp/bmp.go index 08e5a8b6..69451a8d 100644 --- a/internal/format/bmp/bmp.go +++ b/internal/format/bmp/bmp.go @@ -61,6 +61,7 @@ func init() { ID: "bmp", Name: "Windows Bitmap", Extension: ".bmp", + MediaType: "image/bmp", // IANA, RFC 7903 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/container.go b/internal/format/container.go new file mode 100644 index 00000000..fd9d209b --- /dev/null +++ b/internal/format/container.go @@ -0,0 +1,113 @@ +package format + +import ( + "sort" + "strings" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + +// What a container is, apart from the format it writes: how it names its +// contents, and the three ways asking a format for contents can be refused. +// Split out of format.go on 2026-10-07, when eml - the first container that is +// not an archive - pushed that file past the size the shape guard allows. + +// Members is how a container names the settings for contents of one format. +type Members struct { + Count, Format, Size string +} + +// NotAContainerError is contains asked of a format that holds nothing. +type NotAContainerError struct { + Format string + Containers []string +} + +// What happened, what can do it instead, and what to do about it. +func (e *NotAContainerError) What() string { return e.what().String() } + +func (e *NotAContainerError) what() core.Said { + return core.Says("format.NotAContainer", "%s holds no other files, so it cannot take contains", core.A("Format", e.Format)) +} + +func (e *NotAContainerError) Why() string { return e.why().String() } + +func (e *NotAContainerError) why() core.Said { + return core.Says("format.NotAContainerWhy", "the formats that can are %s", core.A("Containers", strings.Join(e.Containers, ", "))) +} + +func (e *NotAContainerError) Instead() string { return e.instead().String() } + +func (e *NotAContainerError) instead() core.Said { + return core.Says("format.NotAContainerFix", "Drop contains, or change the format") +} + +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *NotAContainerError) Parts() (what, why, instead core.Said) { + return e.what(), e.why(), e.instead() +} + +func (e *NotAContainerError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NotAContainerError) Said() core.Said { + return core.Says("format.NotAContainerWhole", "%s - %s. %s", core.A("What", e.what()), core.A("Why", e.why()), core.A("Fix", e.instead())) +} + +// ContentsConflictError is contains stated beside format properties saying the +// same thing. Picking one would build a container holding something other than +// what the recipe says, and the recipe is what somebody reads in a review. +// +// It speaks of files rather than of an archive since a mail message became a +// container (review of #173). +type ContentsConflictError struct { + Format string + Keys []string +} + +func (e *ContentsConflictError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *ContentsConflictError) Said() core.Said { + if len(e.Keys) == 1 { + return core.Says("format.ContentsConflictOne", + "%s: contains and the %s property both say which files it holds. Keep contains and drop the property, or the other way round", + core.A("Format", e.Format), core.A("Key", e.Keys[0])) + } + return core.Says("format.ContentsConflict", + "%s: contains and the %s properties both say which files it holds. Keep contains and drop the properties, or the other way round", + core.A("Format", e.Format), core.A("Keys", strings.Join(e.Keys, ", "))) +} + +// NestingUnsupportedError is a container asked to hold its own format. +// +// A legitimate test case that needs a depth limit before it is allowed, and +// there is none yet. It says that rather than pretending the format is unknown. +type NestingUnsupportedError struct { + Format string +} + +func (e *NestingUnsupportedError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *NestingUnsupportedError) Said() core.Said { + return core.Says("format.NestingUnsupported", + "%s cannot hold %s yet - a file inside a file of its own format needs a depth limit first. Hold a different format, or make the inner file its own target", + core.A("Format", e.Format), core.A("Inner", e.Format)) +} + +// Containers lists the formats that accept contains, for a message that tells +// somebody what to write instead. +func Containers() []string { + mu.RLock() + defer mu.RUnlock() + var out []string + for id, d := range registry { + if d.Container != nil { + out = append(out, id) + } + } + sort.Strings(out) + return out +} diff --git a/internal/format/csvfile/csv.go b/internal/format/csvfile/csv.go index 9d5898b8..778ac5d5 100644 --- a/internal/format/csvfile/csv.go +++ b/internal/format/csvfile/csv.go @@ -105,6 +105,7 @@ func init() { ID: "csv", Name: "Comma-Separated Values", Extension: ".csv", + MediaType: "text/csv", // IANA, RFC 4180 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/docx/docx.go b/internal/format/docx/docx.go index 5fbd9258..81daa80a 100644 --- a/internal/format/docx/docx.go +++ b/internal/format/docx/docx.go @@ -46,6 +46,7 @@ func init() { ID: "docx", Name: "Word (Office Open XML)", Extension: ".docx", + MediaType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // IANA Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/eml/attach.go b/internal/format/eml/attach.go new file mode 100644 index 00000000..b7ae8201 --- /dev/null +++ b/internal/format/eml/attach.go @@ -0,0 +1,286 @@ +package eml + +import ( + "encoding/base64" + "fmt" + "io" + "strings" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/format" +) + +const ( + // base64Line is the longest line of base64 RFC 2045 allows. + base64Line = 76 + + // maxMessage is the largest message this format plans. Far above anything + // a disk holds, and far enough below the int64 every size here is counted + // in that adding the parts up cannot wrap round. + maxMessage = int64(1) << 60 + + // namePrefixWide is what the name of an attached file starts with when + // headers asks for names outside ASCII. + namePrefixWide = "Załącznik_日本語_" + + // percentRun is how much of a name RFC 2231 percent encoding puts on one + // line before it continues on the next, short enough that the line stays + // under 78 characters and cut never inside a %XX. + percentRun = 48 +) + +// child is one attached file, planned before a byte is written. +type child struct { + desc format.Descriptor + plan format.Plan + // name is the file's name as a person reads it. + name string + // contentType and disposition are the two header values, parameters and + // folding included, worked out once while planning. + contentType, disposition string + // encoded is how many bytes the file takes in base64 with its line + // breaks - not the header lines above it, and not the line break the next + // boundary brings. + encoded int64 +} + +// planChildren plans every file the message carries. +// +// The files are real files of another format, each valid on its own, with +// seeds derived from the message's seed and their place in it (RC4), numbered +// per format across the whole message - the archives' rules, for their +// reasons (zip/children.go). +func planChildren(r format.Request, groups []format.Content, s settings) ([]child, error) { + total := 0 + for _, g := range groups { + total += g.Count + } + p := planner{r: r, s: s, out: make([]child, 0, total), numbered: map[string]int{}} + for _, g := range groups { + desc, err := format.Get(g.Format) + if err != nil { + return nil, err + } + if err := p.add(desc, g); err != nil { + return nil, err + } + } + return p.out, nil +} + +// planner is planChildren's place in the message as it goes: how many files +// came before, how many of each format, and how many bytes of base64 so far. +type planner struct { + r format.Request + s settings + out []child + numbered map[string]int + sum int64 +} + +// add plans the files of one group. +func (p *planner) add(desc format.Descriptor, g format.Content) error { + for i := 0; i < g.Count; i++ { + cp, err := desc.Generator.Plan(format.Request{Bytes: g.Bytes, Seed: core.FileSeed(p.r.Seed, len(p.out)), Label: p.r.Label}) + if err != nil { + return core.Refuse(core.Says("eml.TheAttachedFileCannotBe", "eml: the attached %s file cannot be made: %w", core.A("Format", g.Format), core.A("Err", err))) + } + if cp.Bytes > maxMessage { + return tooLarge(cp.Bytes) + } + p.numbered[g.Format]++ + c := child{desc: desc, plan: cp, name: fileName(p.s, g.Format, p.numbered[g.Format], desc.Extension)} + c.encoded = encodedLength(cp.Bytes, len(p.s.eol)) + if p.sum += c.encoded; p.sum > maxMessage { + return tooLarge(p.sum) + } + c.contentType, c.disposition = nameHeaders(p.s, desc.MediaType, c.name) + p.out = append(p.out, c) + } + return nil +} + +// tooLarge refuses attachments that add up past maxMessage. +func tooLarge(asked int64) error { + return &format.AboveMaximumError{ + Format: "EML", + Requested: asked, + Maximum: maxMessage, + Reason: core.Says("eml.AMessageThisLargeCannotBe", "a message this large cannot be counted to the byte in this build"), + Hint: core.Says("eml.AskForSmallerAttachments", "Ask for fewer or smaller attachments."), + } +} + +// encodedLength is how many bytes n bytes take in base64 lines of 76 +// characters, the breaks between them included and none after the last. +func encodedLength(n int64, eol int) int64 { + chars := (n + 2) / 3 * 4 + if chars == 0 { + return 0 + } + return chars + (chars-1)/base64Line*int64(eol) +} + +// fileName is the name of an attached file: the format, its number among the +// files of that format, and the extension - with a prefix in Polish and +// Japanese when headers asks for names outside ASCII. +func fileName(s settings, id string, n int, ext string) string { + name := fmt.Sprintf("%s_%04d%s", id, n, ext) + if s.headers != ASCII { + name = namePrefixWide + name + } + return name +} + +// nameHeaders are the Content-Type and Content-Disposition values of one +// attached file, with its name where filename_style puts it and written the +// way headers says. Each parameter goes on a line of its own, so no name pushes +// a line past 78 characters. docs/EML-2026-10-07.md section 10.3 has the table. +func nameHeaders(s settings, mediaType, name string) (contentType, disposition string) { + var inType, inDisposition []string + switch s.names { + case RFC2231: + inDisposition = rfc2231Params(s, name) + case RFC2047: + inType = []string{quotedParam(s, "name", name)} + inDisposition = []string{quotedParam(s, "filename", name)} + case Both: + inType = []string{quotedParam(s, "name", name)} + inDisposition = rfc2231Params(s, name) + default: + inType = []string{quotedParam(s, "name", name)} + } + return withParams(s, mediaType, inType), withParams(s, "attachment", inDisposition) +} + +// withParams is a header value with its parameters, each folded onto a line +// of its own. +func withParams(s settings, value string, params []string) string { + for _, p := range params { + value += ";" + s.eol + " " + p + } + return value +} + +// quotedParam is a parameter in quotes: the name as it is when it is ASCII or +// when utf8 asks for it raw, and RFC 2047 encoded words, folded, when encoded +// asks for them. Words inside quotes are what RFC 2047 forbids and what +// filename_style rfc2047 exists to hand a tester. +func quotedParam(s settings, key, name string) string { + return key + `="` + phrase(s, name) + `"` +} + +// rfc2231Params is the name as RFC 2231 writes it: in quotes when it is ASCII +// or raw, and percent encoded in UTF-8 otherwise, continued over numbered +// parameters when it is long. +func rfc2231Params(s settings, name string) []string { + if s.headers != Encoded || isASCII(name) { + return []string{`filename="` + name + `"`} + } + encoded := percentEncoded(name) + if len(encoded) <= percentRun { + return []string{"filename*=UTF-8''" + encoded} + } + var out []string + for n := 0; encoded != ""; n++ { + cut := min(percentRun, len(encoded)) + // Never inside a %XX - step back to before the percent sign. + if i := strings.LastIndexByte(encoded[:cut], '%'); i > cut-3 && cut < len(encoded) { + cut = i + } + prefix := "" + if n == 0 { + prefix = "UTF-8''" + } + out = append(out, fmt.Sprintf("filename*%d*=%s%s", n, prefix, encoded[:cut])) + encoded = encoded[cut:] + } + return out +} + +// percentEncoded is RFC 2231 section 7's encoding: the characters a parameter +// may carry as they are, every other byte as %XX. +func percentEncoded(text string) string { + const hex = "0123456789ABCDEF" + var b strings.Builder + for i := 0; i < len(text); i++ { + c := text[i] + if c > ' ' && c < 0x7f && !strings.ContainsRune(`*'%()<>@,;:\"/[]?=`, rune(c)) { + b.WriteByte(c) + continue + } + b.WriteByte('%') + b.WriteByte(hex[c>>4]) + b.WriteByte(hex[c&15]) + } + return b.String() +} + +// attach writes one attached file: its header lines, a blank line and the +// file itself in base64, made by its own generator as it is written. +func (m *message) attach(o *out, c *child) { + eol := m.s.eol + o.put("Content-Type: ", c.contentType, eol) + o.put("Content-Disposition: ", c.disposition, eol) + o.put("Content-Transfer-Encoding: base64", eol, eol) + if o.w == nil || o.err != nil { + o.n += c.encoded + return + } + o.lines = lines{w: o.w, eol: eol} + enc := base64.NewEncoder(base64.StdEncoding, &o.lines) + if err := c.desc.Generator.Write(o.ctx, enc, c.plan); err != nil { + o.err = err + return + } + if err := enc.Close(); err != nil { + o.err = err + return + } + o.n += o.lines.n + if o.lines.n != c.encoded { + o.err = core.Defect(fmt.Errorf("eml: the attached %s file came out as %d B of base64 where its plan gives %d B", + c.desc.ID, o.lines.n, c.encoded)) + } +} + +// lines cuts base64 into lines of 76 characters. The break goes in front of +// the next character rather than after the 76th, so the last line never ends +// in one - the boundary that follows brings its own. +type lines struct { + w io.Writer + eol string + col int + n int64 +} + +func (l *lines) Write(p []byte) (int, error) { + written := 0 + for len(p) > 0 { + if err := l.breakFull(); err != nil { + return written, err + } + k := min(base64Line-l.col, len(p)) + if err := core.WriteAll(l.w, p[:k]); err != nil { + return written, err + } + l.col += k + l.n += int64(k) + written += k + p = p[k:] + } + return written, nil +} + +// breakFull ends a line that holds 76 characters, before the next one starts. +func (l *lines) breakFull() error { + if l.col != base64Line { + return nil + } + if _, err := io.WriteString(l.w, l.eol); err != nil { + return err + } + l.n += int64(len(l.eol)) + l.col = 0 + return nil +} diff --git a/internal/format/eml/eml.go b/internal/format/eml/eml.go new file mode 100644 index 00000000..2cfd2025 --- /dev/null +++ b/internal/format/eml/eml.go @@ -0,0 +1,213 @@ +// Package eml generates mail messages (RFC 5322 with MIME): a text, in plain +// text, HTML or both, and real files of other formats attached to it in +// base64. +// +// What makes this format worth its place is what a system that takes mail in +// does with it - a helpdesk turning messages into tickets, a gateway scanning +// attachments, an archive keeping them. The size a limit is checked against +// is the message, a third larger than the files it carries. The names of the +// files can be written four ways, and readers do not agree on two of them. +// docs/EML-2026-10-07.md has the measurement this is built on. +package eml + +import ( + "context" + "fmt" + "io" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/archive" +) + +const generatorVersion = "1" + +func init() { + format.Register(format.Descriptor{ + ID: "eml", + Name: "Email message", + Extension: ".eml", + MediaType: "message/rfc822", // IANA, RFC 2046 + Fidelity: format.FidelityFull, + Determinism: format.DeterminismByte, + + // The header lines and an empty text, for the default settings. Every + // other combination answers for itself through the same refusal, the + // way json and geojson declare their default layout's minimum. + MinBytes: minimumBytes(), + + Padding: format.PaddingChannel{ + Name: "the words of the text, in every text part", + // Before the attached files when there are any, at the end when + // there are none - so inside, where the format decides. + Where: format.PlacementInside, + Capacity: 0, + }, + Label: format.LabelVisible, + Oracle: "python-email", + Properties: properties(), + Container: &members, + GeneratorVersion: generatorVersion, + Generator: generator{}, + }) +} + +type generator struct{} + +// minimumBytes is the smallest message the default settings make, measured by +// the code that writes it. +func minimumBytes() int64 { + s, err := parse(nil) + if err != nil { + panic(fmt.Sprintf("eml: the default settings do not parse: %v", err)) + } + return newMessage(s, 0, nil).size() +} + +func (generator) Plan(r format.Request) (format.Plan, error) { + s, err := parse(r.Properties) + if err != nil { + return format.Plan{}, err + } + groups, err := archive.GroupsThrough("eml", r, door) + if err != nil { + return format.Plan{}, err + } + children, err := planChildren(r, groups, s) + if err != nil { + return format.Plan{}, err + } + m := newMessage(s, r.Seed, children) + + var notes []format.Note + target := r.Bytes + if r.SizeFromContents { + target = settle(m, r) + } else if notes, err = fit(m, r); err != nil { + return format.Plan{}, err + } + share(m, target-m.size()) + if got := m.size(); got != target { + return format.Plan{}, core.Defect(fmt.Errorf("eml: the message comes to %d B and %d B were planned", got, target)) + } + p := describe(m, groups, target) + p.Notes = notes + return p, nil +} + +// fit makes room for the label in a message of the size asked for, or says +// why the size is too small. A label that does not fit is left out with a +// note, as txt leaves it out - the minimum is the message without it. +func fit(m *message, r format.Request) ([]format.Note, error) { + bare := m.size() + if r.Bytes < bare { + // The reason a tester needs is the base64, when there is anything + // attached - and a sentence about attached files is wrong for a + // message that has none. + reason := core.Says("eml.AMessageHoldsItsHeaderLinesAndText", "a message holds its header lines and the lines that open its text") + if len(m.children) > 0 { + reason = core.Says("eml.AMessageHoldsItsHeaderLines", "a message holds its header lines, its parts and every attached file in base64, "+ + "which is about a third larger than the file itself") + } + return nil, &format.BelowMinimumError{ + Format: "EML", + Requested: r.Bytes, + Minimum: bare, + Reason: reason, + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", bare)), + } + } + if !r.Label { + return nil, nil + } + m.label = core.Label("eml", r.Bytes, r.Seed) + if m.size() <= r.Bytes { + return nil, nil + } + cost := m.size() - bare + m.label = "" + // Silence is banned - a file the user believes carries a label has to say + // that it does not. + return []format.Note{{ + Code: "label_omitted", + Detail: core.Says("format.TheLabelNeedsBAndThe", "The label needs %d B and the file is %d B, so this file carries no label. Its name and the manifest still identify it.", + core.A("Length", cost), core.A("Bytes", r.Bytes)), + }}, nil +} + +// settle is the size of a message whose size comes from what it carries: no +// words beyond the label, and the label states the size. A longer number makes +// a longer label, so the two are settled against each other - the size only +// grows, so it settles in a few rounds. +func settle(m *message, r format.Request) int64 { + size := m.size() + for round := 0; r.Label && round < 8; round++ { + m.label = core.Label("eml", size, r.Seed) + next := m.size() + if next == size { + break + } + size = next + } + return size +} + +// share hands the bytes still owed to the words of the text. alternative has +// the same words in both parts, so it takes them two at a time, and an odd one +// becomes the space parity puts in the HTML. +func share(m *message, rest int64) { + if m.s.body != Alternative { + m.text = rest + return + } + m.parity = rest%2 == 1 + m.text = rest / 2 +} + +// describe is the plan and what reaches the manifest - public names under +// rule 10, approved on 2026-10-07 (docs/EML-2026-10-07.md section 10.5). +func describe(m *message, groups []format.Content, target int64) format.Plan { + var raw, encoded int64 + for _, c := range m.children { + raw += c.plan.Bytes + encoded += c.encoded + } + p := format.Plan{ + Bytes: target, + Exact: true, + Determinism: format.DeterminismByte, + Properties: map[string]any{ + Body: m.s.body, + TextEncoding: m.s.encoding, + Headers: m.s.headers, + LineEndings: m.s.endings, + FilenameStyle: m.s.names, + Attachments: len(m.children), + "contains": archive.ContentSummary(groups), + "attachment_bytes": raw, + "attachment_encoded_bytes": encoded, + "mime_entities": m.entities(), + "subject": m.id.subject, + "message_id": m.id.messageID, + format.PropertyLabelEmbedded: m.label != "", + }, + Memo: m, + } + // The one format shape keeps the keys the archives give it, so a test + // written against one container reads the other the same way. + if len(groups) == 1 { + p.Properties[AttachmentFormat] = groups[0].Format + p.Properties[AttachmentSize] = groups[0].Bytes + } + return p +} + +func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { + m, ok := p.Memo.(*message) + if !ok { + return core.Defect(fmt.Errorf("eml: the plan was not produced by this generator")) + } + o := out{w: w, ctx: ctx, eol: m.s.eol, scratch: make([]byte, 0, chunkSize+64)} + m.emit(&o) + return o.err +} diff --git a/internal/format/eml/header.go b/internal/format/eml/header.go new file mode 100644 index 00000000..b50e2180 --- /dev/null +++ b/internal/format/eml/header.go @@ -0,0 +1,149 @@ +package eml + +import ( + "encoding/base64" + "fmt" + "strings" + "time" + "unicode/utf8" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + +// The people a message is between. Addresses only in the domains RFC 2606 +// keeps for examples, so a message that leaves a test by mistake reaches +// nobody. +const ( + fromAddress = "alice@example.com" + toAddress = "bob@example.org" + + fromASCII = "Alice Example" + toASCII = "Bob Example" + + // The same people with names outside ASCII, for headers set to encoded or + // utf8: Polish for one, Japanese for the other, so a reader that handles + // two byte letters and not three byte ones is caught as well. + fromWide = "Łukasz Żółć" + toWide = "山田 太郎" + + subjectASCII = "Test message" + subjectWide = "Zgłoszenie testowe 日本語" + + // wordBytes is how much text one encoded word carries. Thirty bytes of + // UTF-8 are forty characters of base64 and fifty two with the markers, so + // the first word fits beside "Subject: " under 78 characters and no word + // comes near the 75 RFC 2047 allows. + wordBytes = 30 +) + +// dateWindow is the year a message is dated in, from the start of 2026. The +// date comes from the seed rather than the clock, because the same recipe has +// to give the same bytes (D11). +const dateWindow = 365 * 24 * 60 * 60 + +var dateEpoch = time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() + +// identity is what makes one message this message, worked out from its seed. +// +// Every piece has a fixed length whatever the seed, so the size of a message +// does not depend on which seed it was made with - the minimum is one number +// for one set of settings. +type identity struct { + // messageID is the Message-ID line's value, angle brackets and all, + // because that is how a test finds the ticket the message became. + messageID string + date string + // subject is the subject as a person reads it, before any encoding. + subject string + // boundary separates the parts of multipart/mixed and inner those of + // multipart/alternative. Both carry "=_", which neither base64 nor + // quoted-printable can produce, and they differ in their last letter so + // that neither is the start of the other - RFC 2046 lets a reader match a + // boundary at the start of a longer line. + boundary, inner string + // from, to and subjectLine are the header values as they are written, + // encoded words and folding included. Worked out once here, because the + // message is counted several times while it is planned and a header built + // at every count is an allocation the resource guard counts too. + from, to, subjectLine string +} + +func identify(seed uint64, s settings) identity { + hex := fmt.Sprintf("%016x", seed) + subject, from, to := subjectASCII, fromASCII, toASCII + if s.headers != ASCII { + subject, from, to = subjectWide, fromWide, toWide + } + subject += " " + core.SeedLabel(seed) + return identity{ + messageID: "<" + hex + "@example.com>", + date: time.Unix(dateEpoch+int64(seed%dateWindow), 0).UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"), + subject: subject, + boundary: "=_tfg_" + hex + "_m", + inner: "=_tfg_" + hex + "_a", + from: phrase(s, from) + " <" + fromAddress + ">", + to: phrase(s, to) + " <" + toAddress + ">", + subjectLine: phrase(s, subject), + } +} + +// head writes the lines every message opens with. +func (m *message) head(o *out) { + eol := m.s.eol + o.put("From: ", m.id.from, eol) + o.put("To: ", m.id.to, eol) + o.put("Date: ", m.id.date, eol) + o.put("Message-ID: ", m.id.messageID, eol) + o.put("Subject: ", m.id.subjectLine, eol) + o.put("MIME-Version: 1.0", eol) +} + +// phrase is text for a header line, written the way headers says: as it is +// when it is ASCII or when utf8 asks for it raw, and as encoded words folded +// onto lines of their own when encoded asks for them. +func phrase(s settings, text string) string { + if s.headers != Encoded || isASCII(text) { + return text + } + return strings.Join(encodedWords(text), s.eol+" ") +} + +// encodedWords is text as RFC 2047 B words, cut between characters and never +// inside one, so each word decodes on its own as the RFC requires. +// +// Written here rather than taken from mime.WordEncoder. Its way of cutting is +// the standard library's to change, and a cut moved by a newer Go would move +// the bytes of every message with a name outside ASCII (D11, O169). +func encodedWords(text string) []string { + var out []string + for start := 0; start < len(text); { + end := wordEnd(text, start) + out = append(out, "=?UTF-8?B?"+base64.StdEncoding.EncodeToString([]byte(text[start:end]))+"?=") + start = end + } + return out +} + +// wordEnd is where the encoded word starting at start ends: as many whole +// characters as fit in wordBytes. +func wordEnd(text string, start int) int { + end := start + for end < len(text) { + _, size := utf8.DecodeRuneInString(text[end:]) + if end+size-start > wordBytes { + break + } + end += size + } + return end +} + +// isASCII says whether text needs nothing of headers to be written. +func isASCII(text string) bool { + for i := 0; i < len(text); i++ { + if text[i] >= utf8.RuneSelf { + return false + } + } + return true +} diff --git a/internal/format/eml/message.go b/internal/format/eml/message.go new file mode 100644 index 00000000..99e53f00 --- /dev/null +++ b/internal/format/eml/message.go @@ -0,0 +1,186 @@ +package eml + +import ( + "context" + "io" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + +// message is one message as it will be written, worked out while planning. +// +// The same code counts it and writes it: emit runs against an out with no +// writer to find the size, and against the file to produce it. A size worked +// out by a second copy of the layout would agree with the bytes only for as +// long as somebody kept the two alike by hand. +type message struct { + s settings + id identity + children []child + seed uint64 + // label is the label line's text, or nothing for a message without one. + label string + // text is how many bytes of words each text part carries, and parity is + // the one byte alternative cannot split between its two parts - a space + // in the HTML, where it changes nothing a person sees. + text int64 + parity bool + // plainLine and htmlLine are the line in Polish and Japanese as each part + // writes it, encoded once rather than at every count. + plainLine, htmlLine string +} + +func newMessage(s settings, seed uint64, children []child) *message { + return &message{ + s: s, id: identify(seed, s), children: children, seed: seed, + plainLine: s.internationalLine("", ""), + htmlLine: s.internationalLine("

", "

"), + } +} + +// size is how many bytes the message comes to as it stands. +func (m *message) size() int64 { + o := out{eol: m.s.eol} + m.emit(&o) + return o.n +} + +// entities is every MIME entity in the message, the message itself included - +// the number mailparser counts and refuses past a thousand (measured +// 2026-10-07, docs/EML-2026-10-07.md section 5.1). +func (m *message) entities() int { + text := 1 + if m.s.body == Alternative { + text = 3 + } + if len(m.children) == 0 { + return text + } + return 1 + text + len(m.children) +} + +// emit writes the message: the header lines, then the text alone, or the text +// and every attached file as the parts of multipart/mixed. +func (m *message) emit(o *out) { + m.head(o) + if len(m.children) == 0 { + m.content(o, true) + return + } + eol, b := m.s.eol, m.id.boundary + o.put(`Content-Type: multipart/mixed; boundary="`, b, `"`, eol, eol) + o.put("--", b, eol) + m.content(o, false) + for i := range m.children { + if o.cancelled() { + return + } + o.put(eol, "--", b, eol) + m.attach(o, &m.children[i]) + } + o.put(eol, "--", b, "--", eol) +} + +// content writes the text as one entity: its header lines, a blank line and +// what it says. top is a message with nothing attached, where the text is the +// whole message and a closing boundary ends the file with a line break. +func (m *message) content(o *out, top bool) { + switch m.s.body { + case Plain: + m.textHead(o, "text/plain") + m.plain(o) + case HTML: + m.textHead(o, "text/html") + m.html(o) + default: + eol, in := m.s.eol, m.id.inner + o.put(`Content-Type: multipart/alternative; boundary="`, in, `"`, eol, eol) + o.put("--", in, eol) + m.textHead(o, "text/plain") + m.plain(o) + o.put(eol, "--", in, eol) + m.textHead(o, "text/html") + m.html(o) + o.put(eol, "--", in, "--") + if top { + o.put(eol) + } + } +} + +func (m *message) textHead(o *out, contentType string) { + eol := m.s.eol + o.put("Content-Type: ", contentType, "; charset=", m.s.charset(), eol) + o.put("Content-Transfer-Encoding: ", m.s.encoding, eol, eol) +} + +// plain is the text part: the label, the line in Polish and Japanese, and the +// words that bring the message to its size. +func (m *message) plain(o *out) { + if m.label != "" { + o.put(m.label, m.s.eol) + } + o.put(m.plainLine) + fill(o, m.text, m.seed) +} + +// html is the same text as HTML, the words in one paragraph - line breaks +// inside a paragraph are white space, so the lines keep the text's width. +func (m *message) html(o *out) { + eol := m.s.eol + o.put("") + if m.parity { + o.put(" ") + } + o.put("", eol) + if m.label != "" { + o.put("

", m.label, "

", eol) + } + o.put(m.htmlLine, "

", eol) + fill(o, m.text, m.seed) + o.put(eol, "

", eol, "") +} + +// out is where a message goes - a file, or nowhere while it is being counted. +// +// The first error sticks and everything after it is counted and not written, +// so the layout reads as a list of lines rather than a list of checks. +type out struct { + w io.Writer // nil while counting + ctx context.Context + eol string + n int64 + err error + // scratch is the buffer the words are built in, and lines the writer that + // cuts base64, both kept for the whole message rather than made per part. + scratch []byte + lines lines +} + +func (o *out) put(parts ...string) { + for _, p := range parts { + o.n += int64(len(p)) + if o.w != nil && o.err == nil { + _, o.err = io.WriteString(o.w, p) + } + } +} + +func (o *out) bytes(b []byte) { + o.n += int64(len(b)) + if o.w != nil && o.err == nil { + o.err = core.WriteAll(o.w, b) + } +} + +// cancelled says whether the run was interrupted, and makes that the error. +func (o *out) cancelled() bool { + if o.ctx == nil || o.err != nil { + return o.err != nil + } + if err := o.ctx.Err(); err != nil { + o.err = err + return true + } + return false +} diff --git a/internal/format/eml/settings.go b/internal/format/eml/settings.go new file mode 100644 index 00000000..6b97c906 --- /dev/null +++ b/internal/format/eml/settings.go @@ -0,0 +1,231 @@ +package eml + +import ( + "strconv" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/archive" +) + +// Setting names. Public names under rule 10, so each is spelled once. +const ( + Body = "body" + TextEncoding = "text_encoding" + Headers = "headers" + LineEndings = "line_endings" + FilenameStyle = "filename_style" + + Attachments = "attachments" + AttachmentFormat = "attachment_format" + AttachmentSize = "attachment_size" +) + +// The values of body. +const ( + Plain = "plain" + HTML = "html" + Alternative = "alternative" +) + +// The values of text_encoding, spelled the way Content-Transfer-Encoding +// spells them, because that is the line a tester reads them back from. +const ( + SevenBit = "7bit" + EightBit = "8bit" + QuotedPrintable = "quoted-printable" +) + +// The values of headers. +const ( + ASCII = "ascii" + Encoded = "encoded" + UTF8 = "utf8" +) + +// The values of line_endings. +const ( + CRLF = "crlf" + LF = "lf" +) + +// The values of filename_style. Named after the standard each one follows or +// breaks, and content-type after the only header it writes. +const ( + RFC2231 = "rfc2231" + RFC2047 = "rfc2047" + Both = "both" + ContentType = "content-type" +) + +// attachmentsGroup is the block the attachment settings sit in, on both +// surfaces. +const attachmentsGroup = "Attached files" + +// defaultAttachments is none: a message is text first, and a file attached by +// default would put base64 in front of somebody who asked for a mail. +const defaultAttachments = 0 + +// settings is every choice a message is made with, read once in Plan. +type settings struct { + body string + encoding string + headers string + endings string + names string + // eol is what ends every line of the file, and its length is what the + // arithmetic counts - never a two written as a constant. + eol string +} + +// international says whether the text carries the line in Polish and +// Japanese - which is what quoted-printable and 8bit exist for, and what 7bit +// cannot carry at all. +func (s settings) international() bool { return s.encoding != SevenBit } + +// door is how this format names its contents, for archive.GroupsThrough. +var door = archive.Door{ + Members: members, + DefaultCount: defaultAttachments, + Fewer: core.Says("eml.AskForAttachmentsOrFewer", "Ask for %d attachments or fewer.", core.A("Max", archive.MaxMembers)), +} + +var members = format.Members{Count: Attachments, Format: AttachmentFormat, Size: AttachmentSize} + +// parse reads the settings a request carries. +// +// A value outside the declaration has already been refused by the registry, +// which checks every format in one place. The check stays here for the reason +// geojson gives: a guard calls Plan directly, and a generator that trusts its +// input is one registry change away from writing a message nobody ordered. +// The three attachment settings are read by archive.GroupsThrough, beside +// contains, because the two doors share their rules. +func parse(props map[string]string) (settings, error) { + values := map[string]string{} + for _, p := range textProperties() { + v, err := valueOf(props, p) + if err != nil { + return settings{}, err + } + values[p.Name] = v + } + names, err := valueOf(props, filenameStyle()) + if err != nil { + return settings{}, err + } + s := settings{ + body: values[Body], + encoding: values[TextEncoding], + headers: values[Headers], + endings: values[LineEndings], + names: names, + eol: "\r\n", + } + if s.endings == LF { + s.eol = "\n" + } + return s, nil +} + +// valueOf is one setting's value, or its default, refused in the +// declaration's own words when the declaration does not allow it. +func valueOf(props map[string]string, p format.Property) (string, error) { + v, ok := props[p.Name] + if !ok || v == "" { + return p.Default, nil + } + if bad := p.Allows(v); !bad.IsZero() { + return "", &format.PropertyValueError{Format: "eml", Key: p.Name, Value: v, Reason: bad, Remedy: p.Instead()} + } + return v, nil +} + +// properties is the declaration, in the order a person meets it: what the +// message says and how it is written, then what it carries. +func properties() []format.Property { + return append(textProperties(), attachmentProperties()...) +} + +// textProperties are the settings of the text and of the header lines. +func textProperties() []format.Property { + return []format.Property{ + { + Name: Body, Kind: format.PropertyChoice, + Choices: []string{Alternative, HTML, Plain}, + Default: Plain, + Detail: "Which text the message carries. plain is one text part and html one HTML part. " + + "alternative carries both with the same words, and a mail program shows one of them.", + }, + { + Name: TextEncoding, Kind: format.PropertyChoice, + Choices: []string{SevenBit, EightBit, QuotedPrintable}, + Default: SevenBit, + Detail: "How the text is written. 7bit keeps it to plain ASCII. " + + "quoted-printable and 8bit add a line in Polish and Japanese, because that is what they are for - " + + "quoted-printable spells those letters as =C5=BC, and 8bit writes them as they are.", + }, + { + Name: Headers, Kind: format.PropertyChoice, + Choices: []string{ASCII, Encoded, UTF8}, + Default: ASCII, + Detail: "Whether the subject, the names of the sender and the recipient and the names of attached files " + + "carry letters outside ASCII, and how. encoded writes them the way RFC 2047 and RFC 2231 say. " + + "utf8 writes them as they are (RFC 6532), and Python's email package reads that and reports a defect in the From and To lines.", + }, + { + Name: LineEndings, Kind: format.PropertyChoice, + Choices: []string{CRLF, LF}, + Default: CRLF, + Detail: "How every line of the file ends. The standard is crlf. " + + "Python, MimeKit, mailparser and Go's standard library all read lf too, " + + "so the difference shows in programs that split the lines themselves.", + }, + } +} + +// attachmentProperties are the settings of what the message carries. +func attachmentProperties() []format.Property { + return []format.Property{ + { + Name: Attachments, Kind: format.PropertyInt, + Min: 0, Max: archive.MaxMembers, + Default: strconv.Itoa(defaultAttachments), + Detail: "How many files the message carries. Use contains instead when the files are not all alike. " + + "mailparser refuses a message of more than 1000 MIME entities, and the manifest gives this one's count as mime_entities.", + Group: attachmentsGroup, + }, + { + Name: AttachmentFormat, Kind: format.PropertyText, + Shape: "the id of a format, as tfg formats lists them", + // Not a choice, for the reason the archives give: the allowed + // values are whatever this build registered. + Default: archive.DefaultFormat, + Detail: "The format of the attached files. Run tfg formats to see what this build supports.", + Group: attachmentsGroup, + }, + { + Name: AttachmentSize, Kind: format.PropertySize, + Default: archive.DefaultSizeText, + Detail: "How big each attached file is. The message grows by about a third more, " + + "because attached files travel in base64.", + Group: attachmentsGroup, + }, + filenameStyle(), + } +} + +// filenameStyle is the one attachment setting this package reads itself. +func filenameStyle() format.Property { + return format.Property{ + Name: FilenameStyle, Kind: format.PropertyChoice, + Choices: []string{Both, ContentType, RFC2047, RFC2231}, + Default: RFC2231, + Detail: "How the names of attached files are written. rfc2231 is the standard way. " + + "rfc2047 is the encoded form RFC 2047 forbids inside a name, and many readers decode it anyway. " + + "both writes the two, and content-type puts the name only where old mail programs looked for it. " + + "Go's standard library reads no name from content-type and the encoded text itself from rfc2047. " + + "Unless headers is encoded a name is written as it is and only where it goes changes, " + + "so rfc2047 and both write the same bytes.", + Group: attachmentsGroup, + } +} diff --git a/internal/format/eml/text.go b/internal/format/eml/text.go new file mode 100644 index 00000000..69ff5f4c --- /dev/null +++ b/internal/format/eml/text.go @@ -0,0 +1,155 @@ +package eml + +import ( + "strings" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + +// The text of a message is where the padding goes, decided on 2026-10-07 after +// both candidates were measured (docs/EML-2026-10-07.md section 5.1): every +// reader keeps the text, a system that saves the message again keeps it too, +// and a message of one part has nowhere else to put it. The epilogue after the +// last boundary held up as well in every reader, and RFC 2046 lets anything in +// between throw it away. + +const ( + // lineWidth is one under the 76 quoted-printable allows a line, so the + // last byte of the text can always become a letter - see fill. + lineWidth = 75 + // chunkSize is how much text is built before it is written. + chunkSize = 32 * 1024 +) + +// international is the line in Polish and Japanese that quoted-printable and +// 8bit add to the text - two byte letters and three byte ones, which is what +// those encodings are for. +const international = "Zażółć gęślą jaźń. 日本語のテキストです。" + +// words are lower case and plain, so no line of the text can begin with +// "From " (which mbox files rewrite), with "--" (which reads as a boundary) or +// with a lone full stop (which ends a message in SMTP), and none holds "=". +var words = []string{ + "account", "address", "after", "again", "answer", "before", "below", "between", + "change", "check", "close", "color", "copy", "date", "detail", "draft", "early", + "every", "field", "file", "first", "follow", "group", "help", "issue", "keep", + "later", "letter", "line", "list", "local", "message", "month", "needed", "note", + "number", "office", "order", "other", "page", "paper", "point", "print", "question", + "ready", "record", "reply", "report", "request", "result", "review", "second", + "sender", "short", "small", "status", "subject", "summary", "table", "thank", + "ticket", "today", "update", "value", "week", "while", "words", "write", "year", +} + +// charset is what the text parts declare. +func (s settings) charset() string { + if s.international() { + return "utf-8" + } + return "us-ascii" +} + +// internationalLine is the line in Polish and Japanese the way the text is +// encoded, or nothing for 7bit. +func (s settings) internationalLine(open, close string) string { + if !s.international() { + return "" + } + line := open + international + close + if s.encoding == QuotedPrintable { + line = quotedPrintable(line, s.eol) + } + return line + s.eol +} + +// quotedPrintable is text in quoted-printable (RFC 2045 section 6.7): every +// byte outside printable ASCII, and the equals sign, as =XX, no line longer +// than 76 characters - soft line breaks where a line would be - and no space +// at the end of a line. +// +// Written here for the reason encodedWords is: the bytes it gives have to be +// these bytes on every Go version. +func quotedPrintable(text, eol string) string { + const hex = "0123456789ABCDEF" + var b strings.Builder + col := 0 + for i := 0; i < len(text); i++ { + c := text[i] + token := string(c) + if c == '=' || c < ' ' || c > '~' || (c == ' ' && i == len(text)-1) { + token = "=" + string(hex[c>>4]) + string(hex[c&15]) + } + if col+len(token) > 75 { + b.WriteString("=" + eol) + col = 0 + } + b.WriteString(token) + col += len(token) + } + return b.String() +} + +// fill writes exactly n bytes of words in lines of at most lineWidth, each +// line but the last ending in eol. +// +// The last byte is a letter, whatever falls there. A space at the end of a +// line is what quoted-printable forbids, and a carriage return cut from its +// line feed is a lone CR that no reader handles the same way twice. Both end +// a line of at most lineWidth, so the letter keeps it within 76. +func fill(o *out, n int64, seed uint64) { + if n <= 0 || o.err != nil { + return + } + if o.w == nil { + o.n += n + return + } + w := wordWriter{rng: core.NewRand(seed), eol: o.eol} + buf := o.scratch[:0] + for remaining := n; remaining > 0 && o.err == nil; { + if o.cancelled() { + return + } + buf = w.chunk(buf[:0], remaining) + if int64(len(buf)) >= remaining { + buf = endInALetter(buf[:remaining]) + } + o.bytes(buf) + remaining -= int64(len(buf)) + } + o.scratch = buf +} + +// wordWriter is the words of one text part as they are laid into lines. +type wordWriter struct { + rng interface{ IntN(int) int } + eol string + col int +} + +// chunk appends words to buf until it holds chunkSize bytes or the bytes still +// owed, whichever comes first. The last word may run past either, and the +// caller cuts it. +func (w *wordWriter) chunk(buf []byte, owed int64) []byte { + for len(buf) < chunkSize && int64(len(buf)) < owed { + word := words[w.rng.IntN(len(words))] + switch { + case w.col > 0 && w.col+1+len(word) > lineWidth: + buf = append(buf, w.eol...) + w.col = 0 + case w.col > 0: + buf = append(buf, ' ') + w.col++ + } + buf = append(buf, word...) + w.col += len(word) + } + return buf +} + +// endInALetter makes the last byte of the text a letter - see fill. +func endInALetter(buf []byte) []byte { + if last := buf[len(buf)-1]; last == ' ' || last == '\r' { + buf[len(buf)-1] = 'x' + } + return buf +} diff --git a/internal/format/format.go b/internal/format/format.go index 662bd803..dfccf7fe 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -235,7 +235,21 @@ type Descriptor struct { // 2026-09-24 as a widening. Recorded in docs/FORMAT-NAMES-2026-09-24.md. Name string - Extension string + Extension string + + // MediaType is what a message, an upload form or a server calls this kind + // of file - application/pdf, image/png - written the way the IANA registry + // spells it, or the WHATWG MIME Sniffing standard for the two formats the + // registry does not list (wav and webm, checked 2026-10-07). + // + // It exists because a mail message carries its attachments under a type, + // and a test that checks the type against the extension needs the one + // the format really has. Nothing else writes it into a file today, so + // declaring or correcting one changes no byte of any other format (D11). + // The source of each value sits beside it in the format's own package. + // docs/EML-2026-10-07.md section 10.3. + MediaType string + Fidelity Fidelity Determinism Determinism MinBytes int64 @@ -260,126 +274,23 @@ type Descriptor struct { // generic "no such property", which reads as a gap in this build. Unsupported []UnsupportedSetting - // AllocCeiling is how many objects this format may allocate producing one - // file, when the flat ceiling every other one meets does not describe it. - // Zero means the flat one applies, which is the case for all but one. - // - // It exists because a borrowed encoder allocates on its own account. The - // hand written generators here sit between 3 and 128 objects a file, and - // gav1d, the AVIF encoder, sits at about a hundred - but gen2brain/jxl - // allocates per block, about 618 000 of them for one 640x480 picture. A - // single ceiling has to fit the heaviest format, so one that fits that one - // would say nothing about the other twenty three. - // - // What the ceiling stands in for is untouched by this: the guard also asks - // each format whether its allocation GROWS with the size of the file - // asked for, and that question is the real one. Every format answers it, - // this one included. Owner's decision, 2026-08-31. - // - // A ratchet, like the coverage threshold and the code shape ceilings: it - // goes down when work makes it lowerable, never up to turn a run green. - AllocCeiling int64 - - // Container says this format holds other files, so a recipe may declare - // contains for it. + // Container is how a container names the settings that say, for files of + // one format, what it holds - how many, of which format, how big. Nil for a + // format that holds no other files, so a recipe may declare contains only + // for a format that sets it. // // Declared rather than inferred. A format that quietly ignored contains // would produce an archive with nothing in it and report success, and // that is the silence rule broken in the worst way - the file looks right // and the test suite believes it. - Container bool -} - -// NotAContainerError is contains asked of a format that holds nothing. -type NotAContainerError struct { - Format string - Containers []string -} - -// What happened, what can do it instead, and what to do about it. -func (e *NotAContainerError) What() string { return e.what().String() } - -func (e *NotAContainerError) what() core.Said { - return core.Says("format.NotAContainer", "%s holds no other files, so it cannot take contains", core.A("Format", e.Format)) -} - -func (e *NotAContainerError) Why() string { return e.why().String() } - -func (e *NotAContainerError) why() core.Said { - return core.Says("format.NotAContainerWhy", "the formats that can are %s", core.A("Containers", strings.Join(e.Containers, ", "))) -} - -func (e *NotAContainerError) Instead() string { return e.instead().String() } - -func (e *NotAContainerError) instead() core.Said { - return core.Says("format.NotAContainerFix", "Drop contains, or change the format") -} - -// Parts is what happened, why and what to do instead, for a reader that lays -// them out apart and in its own language. -func (e *NotAContainerError) Parts() (what, why, instead core.Said) { - return e.what(), e.why(), e.instead() -} - -func (e *NotAContainerError) Error() string { return e.Said().String() } - -// Said is the whole refusal, for a window that says it in its own language. -func (e *NotAContainerError) Said() core.Said { - return core.Says("format.NotAContainerWhole", "%s - %s. %s", core.A("What", e.what()), core.A("Why", e.why()), core.A("Fix", e.instead())) -} - -// ContentsConflictError is contains stated beside format properties saying the -// same thing. Picking one would build an archive holding something other than -// what the recipe says, and the recipe is what somebody reads in a review. -type ContentsConflictError struct { - Format string - Keys []string -} - -func (e *ContentsConflictError) Error() string { return e.Said().String() } - -// Said is the refusal, for a window that says it in its own language. -func (e *ContentsConflictError) Said() core.Said { - if len(e.Keys) == 1 { - return core.Says("format.ContentsConflictOne", - "%s: contains and the %s property both say what the archive holds. Keep contains and drop the properties, or the other way round", - core.A("Format", e.Format), core.A("Key", e.Keys[0])) - } - return core.Says("format.ContentsConflict", - "%s: contains and the %s properties both say what the archive holds. Keep contains and drop the properties, or the other way round", - core.A("Format", e.Format), core.A("Keys", strings.Join(e.Keys, ", "))) -} - -// NestingUnsupportedError is a container asked to hold its own format. -// -// A legitimate test case that needs a depth limit before it is allowed, and -// there is none yet. It says that rather than pretending the format is unknown. -type NestingUnsupportedError struct { - Format string -} - -func (e *NestingUnsupportedError) Error() string { return e.Said().String() } - -// Said is the refusal, for a window that says it in its own language. -func (e *NestingUnsupportedError) Said() core.Said { - return core.Says("format.NestingUnsupported", - "%s cannot hold %s yet - an archive inside an archive needs a depth limit first. Hold a different format, or build the inner archive as its own target", - core.A("Format", e.Format), core.A("Inner", e.Format)) -} - -// Containers lists the formats that accept contains, for a message that tells -// somebody what to write instead. -func Containers() []string { - mu.RLock() - defer mu.RUnlock() - var out []string - for id, d := range registry { - if d.Container { - out = append(out, id) - } - } - sort.Strings(out) - return out + // + // The names are part of the declaration because containers name them + // differently: an archive holds entries and a mail message attachments. + // Until eml arrived on 2026-10-07 every container was an archive, a flag + // said so, and the guards read "entries" by name. The flag and the names + // became one field that day, when the descriptor reached the field count + // the type shape guard watches. + Container *Members } // Allows reports whether raw is a value this property accepts, and says what diff --git a/internal/format/geojson/geojson.go b/internal/format/geojson/geojson.go index feda9d40..317eefc0 100644 --- a/internal/format/geojson/geojson.go +++ b/internal/format/geojson/geojson.go @@ -31,6 +31,7 @@ func init() { ID: "geojson", Name: "GeoJSON", Extension: ".geojson", + MediaType: "application/geo+json", // IANA, RFC 7946 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/gif/gif.go b/internal/format/gif/gif.go index 3a6c1900..36404673 100644 --- a/internal/format/gif/gif.go +++ b/internal/format/gif/gif.go @@ -103,6 +103,7 @@ func init() { ID: "gif", Name: "Graphics Interchange Format", Extension: ".gif", + MediaType: "image/gif", // IANA, RFC 2046 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/htmlfile/html.go b/internal/format/htmlfile/html.go index 745ec27e..33bda56f 100644 --- a/internal/format/htmlfile/html.go +++ b/internal/format/htmlfile/html.go @@ -121,6 +121,7 @@ func init() { ID: "html", Name: "HyperText Markup Language", Extension: ".html", + MediaType: "text/html", // IANA, W3C Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/ico/ico.go b/internal/format/ico/ico.go index 3d3a1240..d61c5e63 100644 --- a/internal/format/ico/ico.go +++ b/internal/format/ico/ico.go @@ -60,6 +60,7 @@ func init() { ID: "ico", Name: "Windows Icon", Extension: ".ico", + MediaType: "image/vnd.microsoft.icon", // IANA Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/jpg/jpg.go b/internal/format/jpg/jpg.go index c73a001a..49c5e78f 100644 --- a/internal/format/jpg/jpg.go +++ b/internal/format/jpg/jpg.go @@ -72,6 +72,7 @@ func init() { ID: "jpg", Name: "JPEG", Extension: ".jpg", + MediaType: "image/jpeg", // IANA, RFC 2046 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/jsonfile/json.go b/internal/format/jsonfile/json.go index 78286763..29761d72 100644 --- a/internal/format/jsonfile/json.go +++ b/internal/format/jsonfile/json.go @@ -63,6 +63,7 @@ func init() { ID: "json", Name: "JavaScript Object Notation", Extension: ".json", + MediaType: "application/json", // IANA, RFC 8259 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/jxl/jxl.go b/internal/format/jxl/jxl.go index e9fb826d..0a84d63b 100644 --- a/internal/format/jxl/jxl.go +++ b/internal/format/jxl/jxl.go @@ -135,21 +135,12 @@ func init() { ID: "jxl", Name: "JPEG XL", Extension: ".jxl", + MediaType: "image/jxl", // IANA, ISO/IEC JTC 1 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, MinBytes: minimumBytes, - // This encoder allocates per block rather than per file, so the flat - // ceiling every hand written format here meets does not describe it. - // Measured on 2026-08-31, lowest of five rounds: 1 222 objects for one - // pixel, 156 679 at 320x240, 618 461 at 640x480. The number below is - // today's measurement at the top of the ladder with room for the - // runtime to move, and it is a ratchet like every other ceiling in - // this project - it goes down when work makes it lowerable, never up - // to turn a run green. - AllocCeiling: 700_000, - Padding: format.PaddingChannel{ // Measured on files this encoder produces, not on a container // built by hand. libjxl through pillow-jxl-plugin and the pure Go @@ -186,6 +177,19 @@ func init() { type generator struct{} +// AllocCeiling is how many objects this generator may allocate for one +// file, for the resource guard - see format.AllocCeiling. +// +// This encoder allocates per block rather than per file, so the flat +// ceiling every hand written format here meets does not describe it. +// Measured on 2026-08-31, lowest of five rounds: 1 222 objects for one +// pixel, 156 679 at 320x240, 618 461 at 640x480. The number below is +// today's measurement at the top of the ladder with room for the +// runtime to move, and it is a ratchet like every other ceiling in +// this project - it goes down when work makes it lowerable, never up +// to turn a run green. +func (generator) AllocCeiling() int64 { return 700_000 } + type memo struct { width, height int quality int diff --git a/internal/format/logfile/log.go b/internal/format/logfile/log.go index 3b95fb8e..8d1eda8f 100644 --- a/internal/format/logfile/log.go +++ b/internal/format/logfile/log.go @@ -50,6 +50,7 @@ func init() { ID: "log", Name: "Server and application log", Extension: ".log", + MediaType: "text/plain", // IANA, RFC 2046 - a log is plain text Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/md/md.go b/internal/format/md/md.go index 92baaa17..6d9f883c 100644 --- a/internal/format/md/md.go +++ b/internal/format/md/md.go @@ -46,6 +46,7 @@ func init() { ID: "md", Name: "Markdown", Extension: ".md", + MediaType: "text/markdown", // IANA, RFC 7763 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/mp4/mp4.go b/internal/format/mp4/mp4.go index 6cdb03f8..2567160f 100644 --- a/internal/format/mp4/mp4.go +++ b/internal/format/mp4/mp4.go @@ -49,22 +49,12 @@ func init() { ID: id, Name: "MP4 video", Extension: ".mp4", + MediaType: "video/mp4", // IANA, RFC 4337 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, MinBytes: minimumBytes(), - // WebM's ceiling and for WebM's reason: every picture is coded on its - // own, a tile a call to gav1d, and gav1d allocates twenty odd objects - // a call (internal/format/webm). The second pass takes every tile the - // first one kept, so the default film calls gav1d no more often than - // its WebM does. Measured on 2026-10-07, lowest of the guard's rounds: - // 938 to 941 objects at one, four and sixteen threads (WebM 898 to - // 904), the sample lengths and the second pass's crew the difference - - // and 1241 with one object allocated per frame, the defect the ceiling - // exists for. 1024 sits between the two. - AllocCeiling: 1024, - Padding: format.PaddingChannel{ Name: "free boxes after the pictures at the end of the file", Where: format.PlacementEnd, @@ -94,6 +84,20 @@ func minimumBytes() int64 { type generator struct{} +// AllocCeiling is how many objects this generator may allocate for one +// file, for the resource guard - see format.AllocCeiling. +// +// WebM's ceiling and for WebM's reason: every picture is coded on its +// own, a tile a call to gav1d, and gav1d allocates twenty odd objects +// a call (internal/format/webm). The second pass takes every tile the +// first one kept, so the default film calls gav1d no more often than +// its WebM does. Measured on 2026-10-07, lowest of the guard's rounds: +// 938 to 941 objects at one, four and sixteen threads (WebM 898 to +// 904), the sample lengths and the second pass's crew the difference - +// and 1241 with one object allocated per frame, the defect the ceiling +// exists for. 1024 sits between the two. +func (generator) AllocCeiling() int64 { return 1024 } + func (generator) Plan(r format.Request) (format.Plan, error) { need := func(st video.Stream) int64 { return newLayout(st).need() } return video.Plan(id, r, need, belowMinimum) diff --git a/internal/format/pdf/pdf.go b/internal/format/pdf/pdf.go index e47abe2d..864ed24b 100644 --- a/internal/format/pdf/pdf.go +++ b/internal/format/pdf/pdf.go @@ -20,6 +20,7 @@ func init() { ID: "pdf", Name: "Portable Document Format", Extension: ".pdf", + MediaType: "application/pdf", // IANA, RFC 8118 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, MinBytes: minimumBytes(), diff --git a/internal/format/png/png.go b/internal/format/png/png.go index 54097fc2..bf2ac5cc 100644 --- a/internal/format/png/png.go +++ b/internal/format/png/png.go @@ -75,6 +75,7 @@ func init() { ID: "png", Name: "Portable Network Graphics", Extension: ".png", + MediaType: "image/png", // IANA, W3C Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/pptx/pptx.go b/internal/format/pptx/pptx.go index 3c9e7066..7c7af6bf 100644 --- a/internal/format/pptx/pptx.go +++ b/internal/format/pptx/pptx.go @@ -56,6 +56,7 @@ func init() { ID: "pptx", Name: "PowerPoint (Office Open XML)", Extension: ".pptx", + MediaType: "application/vnd.openxmlformats-officedocument.presentationml.presentation", // IANA Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/svgfile/svg.go b/internal/format/svgfile/svg.go index 819d765a..8ef54923 100644 --- a/internal/format/svgfile/svg.go +++ b/internal/format/svgfile/svg.go @@ -190,6 +190,7 @@ func init() { ID: "svg", Name: "Scalable Vector Graphics", Extension: ".svg", + MediaType: "image/svg+xml", // IANA, W3C Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/targz/targz.go b/internal/format/targz/targz.go index 4db1c0dd..3e372fa0 100644 --- a/internal/format/targz/targz.go +++ b/internal/format/targz/targz.go @@ -95,6 +95,7 @@ func init() { ID: "targz", Name: "tar + gzip", Extension: ".tar.gz", + MediaType: "application/gzip", // IANA, RFC 6713 - the outer layer is what a reader meets first Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, MinBytes: minimumBytes(), @@ -144,7 +145,7 @@ func init() { Instead: core.Says("format.UseZipForAnArchiveWith", "Use zip for an archive with a password, or leave this one open."), }, }, - Container: true, + Container: &archive.ArchiveMembers, GeneratorVersion: generatorVersion, Generator: generator{}, }) @@ -332,7 +333,7 @@ func describe(target int64, label string, m memo, groups []format.Content) forma Determinism: format.DeterminismByte, Properties: map[string]any{ "entries": len(m.children), - "contains": contentSummary(groups), + "contains": archive.ContentSummary(groups), // Where the files sit, and whether the directories are named - // written every time rather than only when nested, so a harness // never has to read a missing key as flat. @@ -366,20 +367,6 @@ func describe(target int64, label string, m memo, groups []format.Content) forma return p } -// contentSummary is what the archive holds, in the manifest, so a test can -// assert on it without unpacking the file. -func contentSummary(groups []format.Content) []map[string]any { - out := make([]map[string]any, 0, len(groups)) - for _, g := range groups { - out = append(out, map[string]any{ - "format": g.Format, - "count": g.Count, - "bytes": g.Bytes, - }) - } - return out -} - // describeGroups is the same thing for a person reading an error. func describeGroups(groups []format.Content) core.Said { parts := make(core.Conjoined, 0, len(groups)) diff --git a/internal/format/tiff/tiff.go b/internal/format/tiff/tiff.go index 640c6b0e..30a36cdf 100644 --- a/internal/format/tiff/tiff.go +++ b/internal/format/tiff/tiff.go @@ -106,6 +106,7 @@ func init() { ID: "tiff", Name: "Tagged Image File Format", Extension: ".tiff", + MediaType: "image/tiff", // IANA, RFC 3302 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/tomlfile/toml.go b/internal/format/tomlfile/toml.go index b16f0ff2..ff433935 100644 --- a/internal/format/tomlfile/toml.go +++ b/internal/format/tomlfile/toml.go @@ -75,6 +75,7 @@ func init() { ID: "toml", Name: "TOML", Extension: ".toml", + MediaType: "application/toml", // IANA Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/txt/txt.go b/internal/format/txt/txt.go index f5bc661c..b867b907 100644 --- a/internal/format/txt/txt.go +++ b/internal/format/txt/txt.go @@ -36,6 +36,7 @@ func init() { ID: "txt", Name: "Plain text", Extension: ".txt", + MediaType: "text/plain", // IANA, RFC 2046 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/wav/wav.go b/internal/format/wav/wav.go index 5bc99c71..fc4c6f12 100644 --- a/internal/format/wav/wav.go +++ b/internal/format/wav/wav.go @@ -81,6 +81,7 @@ func init() { ID: "wav", Name: "Waveform Audio", Extension: ".wav", + MediaType: "audio/wave", // not in the IANA registry, WHATWG MIME Sniffing Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, MinBytes: baseSize, diff --git a/internal/format/webm/webm.go b/internal/format/webm/webm.go index 4564d533..6f51def1 100644 --- a/internal/format/webm/webm.go +++ b/internal/format/webm/webm.go @@ -39,38 +39,12 @@ func init() { ID: id, Name: "WebM video", Extension: ".webm", + MediaType: "video/webm", // not in the IANA registry, WHATWG MIME Sniffing Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, MinBytes: minimumBytes(), - // Every picture of a film is coded on its own, and gav1d allocates - // per picture - about forty objects each with the frames around it. - // The flat ceiling counts what a generator allocates per file, and a - // default film is ten pictures. Measured on 2026-10-06, lowest of the - // guard's rounds: 408 objects for the default film, and 698 with one - // object allocated per frame, the defect the ceiling exists for. The - // number below sits between the two, and like every ceiling here it - // goes down when work makes it lowerable, never up to turn a run green. - // Since the pictures are coded beside each other (internal/format/video, - // ahead.go) the default film is 474 to 477 objects at sixteen, four and - // one threads alike - each picture is a job, each helper one painter of - // four allocations - measured the same day, still under the ceiling, - // which was not moved. - // - // Raised to 1024 on 2026-10-07 by the owner's decision, the one time it - // went up, and why: a picture is cut into AV1 tiles since then, each - // tile one call to gav1d, and gav1d allocates 21 to 22 objects a call - // whatever the size (tools/probes/videotiles/allocs). The default film, - // 640x360 in six tiles, makes 27 calls - about 594 objects of gav1d's - // alone, over the old ceiling before this package allocates anything. - // With this package's own allocations cut down it is 886 to 891 at one, - // four and sixteen threads, and one object a frame, the defect the - // ceiling exists for, takes it to 1177. 1024 sits between the two, as - // 512 sat between 474 and 698 (docs/WEBM-WYDAJNOSC-2026-10-06.md - // section 10). - AllocCeiling: 1024, - Padding: format.PaddingChannel{ Name: "a Void element before the index at the end of the file", Where: format.PlacementEnd, @@ -100,6 +74,36 @@ func minimumBytes() int64 { type generator struct{} +// AllocCeiling is how many objects this generator may allocate for one +// file, for the resource guard - see format.AllocCeiling. +// +// Every picture of a film is coded on its own, and gav1d allocates +// per picture - about forty objects each with the frames around it. +// The flat ceiling counts what a generator allocates per file, and a +// default film is ten pictures. Measured on 2026-10-06, lowest of the +// guard's rounds: 408 objects for the default film, and 698 with one +// object allocated per frame, the defect the ceiling exists for. The +// number below sits between the two, and like every ceiling here it +// goes down when work makes it lowerable, never up to turn a run green. +// Since the pictures are coded beside each other (internal/format/video, +// ahead.go) the default film is 474 to 477 objects at sixteen, four and +// one threads alike - each picture is a job, each helper one painter of +// four allocations - measured the same day, still under the ceiling, +// which was not moved. +// +// Raised to 1024 on 2026-10-07 by the owner's decision, the one time it +// went up, and why: a picture is cut into AV1 tiles since then, each +// tile one call to gav1d, and gav1d allocates 21 to 22 objects a call +// whatever the size (tools/probes/videotiles/allocs). The default film, +// 640x360 in six tiles, makes 27 calls - about 594 objects of gav1d's +// alone, over the old ceiling before this package allocates anything. +// With this package's own allocations cut down it is 886 to 891 at one, +// four and sixteen threads, and one object a frame, the defect the +// ceiling exists for, takes it to 1177. 1024 sits between the two, as +// 512 sat between 474 and 698 (docs/WEBM-WYDAJNOSC-2026-10-06.md +// section 10). +func (generator) AllocCeiling() int64 { return 1024 } + func (generator) Plan(r format.Request) (format.Plan, error) { need := func(st video.Stream) int64 { return newLayout(st).boundBytes() + minVoid } return video.Plan(id, r, need, belowMinimum) diff --git a/internal/format/webp/webp.go b/internal/format/webp/webp.go index 5a05e9c4..0732e8f9 100644 --- a/internal/format/webp/webp.go +++ b/internal/format/webp/webp.go @@ -81,6 +81,7 @@ func init() { ID: "webp", Name: "WebP", Extension: ".webp", + MediaType: "image/webp", // IANA, RFC 9649 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/work.go b/internal/format/work.go index c844f2dd..db6c5754 100644 --- a/internal/format/work.go +++ b/internal/format/work.go @@ -30,3 +30,27 @@ func Worked(ctx context.Context, n int64) { report(n) } } + +// AllocCeiling is implemented by a generator that may allocate more objects +// producing one file than the flat ceiling every other one meets. A generator +// without it meets the flat one, which is the case for all but three. +// +// It exists because a borrowed encoder allocates on its own account. The hand +// written generators here sit between 3 and 128 objects a file, and gav1d, the +// AVIF encoder, sits at about a hundred - but gen2brain/jxl allocates per +// block, about 618 000 of them for one 640x480 picture. A single ceiling has to +// fit the heaviest format, so one that fits that one would say nothing about +// the others. +// +// What the ceiling stands in for is untouched by this: the guard also asks +// each format whether its allocation GROWS with the size of the file asked for, +// and that question is the real one. Owner's decision, 2026-08-31. A ratchet, +// like the coverage threshold and the code shape ceilings: it goes down when +// work makes it lowerable, never up to turn a run green. +// +// A method of the generator rather than a field of the descriptor since +// 2026-10-07, when the descriptor reached the field count the type shape guard +// watches - how much a generator allocates is the generator's to say. +type AllocCeiling interface { + AllocCeiling() int64 +} diff --git a/internal/format/xlsx/xlsx.go b/internal/format/xlsx/xlsx.go index e2288f06..df7174f0 100644 --- a/internal/format/xlsx/xlsx.go +++ b/internal/format/xlsx/xlsx.go @@ -67,6 +67,7 @@ func init() { ID: "xlsx", Name: "Excel (Office Open XML)", Extension: ".xlsx", + MediaType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // IANA Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/xmlfile/xml.go b/internal/format/xmlfile/xml.go index b20605ad..72bcf384 100644 --- a/internal/format/xmlfile/xml.go +++ b/internal/format/xmlfile/xml.go @@ -88,6 +88,7 @@ func init() { ID: "xml", Name: "Extensible Markup Language", Extension: ".xml", + MediaType: "application/xml", // IANA, RFC 7303 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/yamlfile/yaml.go b/internal/format/yamlfile/yaml.go index 7d4d4752..19a8c75b 100644 --- a/internal/format/yamlfile/yaml.go +++ b/internal/format/yamlfile/yaml.go @@ -104,6 +104,7 @@ func init() { ID: "yaml", Name: "YAML", Extension: ".yaml", + MediaType: "application/yaml", // IANA, RFC 9512 Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, diff --git a/internal/format/zip/zip.go b/internal/format/zip/zip.go index 73a4f687..74ee9d6c 100644 --- a/internal/format/zip/zip.go +++ b/internal/format/zip/zip.go @@ -63,6 +63,7 @@ func init() { ID: "zip", Name: "ZIP", Extension: ".zip", + MediaType: "application/zip", // IANA Fidelity: format.FidelityFull, Determinism: format.DeterminismByte, MinBytes: minimumBytes(), @@ -80,7 +81,7 @@ func init() { Properties: archive.Axes(archive.Entries, archive.EntryFormat, archive.EntrySize, archive.Compression, archive.Depth, archive.DirectoryEntries, archive.Password, archive.Encryption), - Container: true, + Container: &archive.ArchiveMembers, GeneratorVersion: generatorVersion, Generator: generator{}, }) @@ -289,7 +290,7 @@ func describe(target int64, label string, m memo, groups []format.Content) forma Determinism: format.DeterminismByte, Properties: map[string]any{ "entries": len(m.children), - "contains": contentSummary(groups), + "contains": archive.ContentSummary(groups), "method": "store", // Where the files sit, and whether the directories are named. // Written every time rather than only when nested, like method @@ -391,20 +392,6 @@ func pad(m *memo, p *format.Plan, r format.Request, target, bare int64, label st return nil } -// contentSummary is what the archive holds, in the manifest, so a test can -// assert on it without unpacking the file. -func contentSummary(groups []format.Content) []map[string]any { - out := make([]map[string]any, 0, len(groups)) - for _, g := range groups { - out = append(out, map[string]any{ - "format": g.Format, - "count": g.Count, - "bytes": g.Bytes, - }) - } - return out -} - // describeGroups is the same thing for a person reading an error. func describeGroups(groups []format.Content) core.Said { parts := make(core.Conjoined, 0, len(groups)) diff --git a/internal/guard/archiveaxes_test.go b/internal/guard/archiveaxes_test.go index 74bba649..c6593308 100644 --- a/internal/guard/archiveaxes_test.go +++ b/internal/guard/archiveaxes_test.go @@ -30,7 +30,7 @@ import ( func TestEveryContainerDeclaresTheSharedAxesAsTheyAreDeclaredOnce(t *testing.T) { containers := 0 for _, d := range format.All() { - if !d.Container { + if d.Container == nil { continue } containers++ diff --git a/internal/guard/archivecompression_test.go b/internal/guard/archivecompression_test.go index 6644fda6..3d953d97 100644 --- a/internal/guard/archivecompression_test.go +++ b/internal/guard/archivecompression_test.go @@ -32,7 +32,7 @@ func TestACompressedArchiveStillHitsTheSizeToTheByte(t *testing.T) { levels := []string{archive.CompressFast, archive.CompressDefault, archive.CompressBest} checked := 0 for _, d := range format.All() { - if !d.Container { + if !compresses(d) { continue } for _, level := range levels { @@ -82,7 +82,7 @@ func TestACompressedArchiveStillHitsTheSizeToTheByte(t *testing.T) { // to more than the file holds. func TestAskingForCompressionActuallyCompresses(t *testing.T) { for _, d := range format.All() { - if !d.Container { + if !compresses(d) { continue } build := func(level string) []byte { @@ -155,7 +155,7 @@ func TestAskingForCompressionActuallyCompresses(t *testing.T) { // cannot tell whether to drop the compression or give the archive a size. func TestCompressionWithASizeFromTheContentsIsRefusedNamingBoth(t *testing.T) { for _, d := range format.All() { - if !d.Container { + if !compresses(d) { continue } _, err := d.Generator.Plan(format.Request{ @@ -183,7 +183,7 @@ func TestCompressionWithASizeFromTheContentsIsRefusedNamingBoth(t *testing.T) { func TestCompressionWithAPasswordIsRefusedNamingBoth(t *testing.T) { asked := 0 for _, d := range format.All() { - if !d.Container { + if !compresses(d) { continue } if !offers(d, archive.Password) { @@ -218,7 +218,7 @@ func TestCompressionWithAPasswordIsRefusedNamingBoth(t *testing.T) { // a single recipe changing - untouchable rule 3. func TestAnArchiveNobodyAskedToCompressIsStored(t *testing.T) { for _, d := range format.All() { - if !d.Container { + if !compresses(d) { continue } plain, err := d.Generator.Plan(format.Request{Bytes: 1 << 20, Seed: 7741, Label: true}) @@ -274,3 +274,15 @@ func offers(d format.Descriptor, name string) bool { } return false } + +// compresses is whether a container offers the compression setting. Asked +// rather than taken from Container, because a container is not always an +// archive - a mail message holds files too and squeezes none of them. +func compresses(d format.Descriptor) bool { + for _, p := range d.Properties { + if p.Name == archive.Compression { + return true + } + } + return false +} diff --git a/internal/guard/archivecontents_test.go b/internal/guard/archivecontents_test.go index 8b06e9f6..418c0a28 100644 --- a/internal/guard/archivecontents_test.go +++ b/internal/guard/archivecontents_test.go @@ -33,13 +33,13 @@ func TestOnlyAFormatThatHoldsFilesOffersToPutFilesInIt(t *testing.T) { offered := buttonNamed(body, text.ButtonAddContents()) != nil switch { - case d.Container && !offered: + case d.Container != nil && !offered: t.Errorf("%s holds other files and its batch offers no way to say what they are", d.ID) - case !d.Container && offered: + case d.Container == nil && offered: t.Errorf("%s holds no other files and its batch offers to put files in it, "+ "which leads nowhere but a refusal", d.ID) } - if d.Container { + if d.Container != nil { holders++ } else { plain++ diff --git a/internal/guard/archivereadable_test.go b/internal/guard/archivereadable_test.go index de158ca9..d654298e 100644 --- a/internal/guard/archivereadable_test.go +++ b/internal/guard/archivereadable_test.go @@ -44,7 +44,7 @@ import ( func TestEveryArchiveThisToolWritesCanBeReadByTheStandardLibrary(t *testing.T) { checked := 0 for _, d := range format.All() { - if !d.Container { + if d.Container == nil { continue } read, ok := standardReaderFor(d.ID) @@ -103,6 +103,13 @@ func standardReaderFor(id string) (func([]byte) error, bool) { _, err := stdzip.NewReader(bytes.NewReader(b), int64(len(b))) return err }, true + case "eml": + // Read to the last byte of every part, because a message is only + // refused where a part is broken - see emlgo_test.go. + return func(b []byte) error { + _, _, err := goReadMail(b) + return err + }, true } return nil, false } diff --git a/internal/guard/codeshape_test.go b/internal/guard/codeshape_test.go index ba37535e..8b5721c7 100644 --- a/internal/guard/codeshape_test.go +++ b/internal/guard/codeshape_test.go @@ -62,7 +62,10 @@ const ( // name moved out of manifest/manifest.go into reservation.go when O252 // took the file past the ceiling. The longest file is format/zip/zip.go // now. - longestFile = 399 + // Lowered from 399 on 2026-10-07: what makes a container moved out of + // format/format.go into container.go when two fields for eml took it + // past the ceiling. The longest file is gui/window/recipe.go now. + longestFile = 395 // Depth answers a different question than length, and it is the better // question of the two. A hundred line function that is flat reads top to diff --git a/internal/guard/containerentries_test.go b/internal/guard/containerentries_test.go index 7bf2692a..f3c7ba8f 100644 --- a/internal/guard/containerentries_test.go +++ b/internal/guard/containerentries_test.go @@ -30,7 +30,7 @@ import ( func TestBothWaysOfAskingForEntriesShareOneCeiling(t *testing.T) { containers := 0 for _, d := range format.All() { - if !d.Container { + if d.Container == nil { continue } containers++ @@ -44,7 +44,7 @@ func TestBothWaysOfAskingForEntriesShareOneCeiling(t *testing.T) { // value, and the generator's own reading of it is a second line the // engine never reaches. Asking Plan here would compare the two doors at // a layer neither surface uses. - propBad := d.CheckEachProperty(map[string]string{"entries": itoa(over)}) + propBad := d.CheckEachProperty(map[string]string{d.Container.Count: itoa(over)}) var viaProperty error if len(propBad) > 0 { viaProperty = propBad[0] @@ -56,10 +56,10 @@ func TestBothWaysOfAskingForEntriesShareOneCeiling(t *testing.T) { _, viaContains := d.Generator.Plan(req) if viaProperty == nil { - t.Errorf("%s: entries=%d was accepted and the declared ceiling is %d", d.ID, over, ceiling) + t.Errorf("%s: %s=%d was accepted and the declared ceiling is %d", d.ID, d.Container.Count, over, ceiling) } if viaContains == nil { - t.Errorf("%s: contains asking for %d entries was accepted and the declared ceiling is %d", + t.Errorf("%s: contains asking for %d files was accepted and the declared ceiling is %d", d.ID, over, ceiling) continue } @@ -68,11 +68,11 @@ func TestBothWaysOfAskingForEntriesShareOneCeiling(t *testing.T) { // decides the exit code and what a window marks. var propErr, containsErr *format.PropertyValueError if !errors.As(viaProperty, &propErr) { - t.Errorf("%s: the entries refusal is %T, not a refusal about a property value", d.ID, viaProperty) + t.Errorf("%s: the %s refusal is %T, not a refusal about a property value", d.ID, d.Container.Count, viaProperty) } if !errors.As(viaContains, &containsErr) { t.Errorf("%s: the contains refusal is %T, not a refusal about a property value - "+ - "so it lands on a different exit code than the same request through entries", d.ID, viaContains) + "so it lands on a different exit code than the same request through %s", d.ID, viaContains, d.Container.Count) continue } @@ -80,8 +80,8 @@ func TestBothWaysOfAskingForEntriesShareOneCeiling(t *testing.T) { // the reason rather than the whole sentence, because the key differs // on purpose - one says entries and the other says contains. if propErr != nil && propErr.Reason.String() != containsErr.Reason.String() { - t.Errorf("%s: the two doors give different reasons\n entries: %s\n contains: %s", - d.ID, propErr.Reason, containsErr.Reason) + t.Errorf("%s: the two doors give different reasons\n %s: %s\n contains: %s", + d.ID, d.Container.Count, propErr.Reason, containsErr.Reason) } if !strings.Contains(containsErr.Reason.String(), itoa(ceiling)) { t.Errorf("%s: the contains refusal does not name the ceiling %d: %q", @@ -101,7 +101,7 @@ func TestBothWaysOfAskingForEntriesShareOneCeiling(t *testing.T) { // code for before. func TestTheEntryCeilingStillAcceptsTheNumberBelowIt(t *testing.T) { for _, d := range format.All() { - if !d.Container { + if d.Container == nil { continue } ceiling := declaredCeiling(t, d) @@ -135,10 +135,10 @@ func planRequestWith(props map[string]string) format.Request { func declaredCeiling(t *testing.T, d format.Descriptor) int { t.Helper() for _, p := range d.Properties { - if p.Name == "entries" { + if p.Name == d.Container.Count { return int(p.Max) } } - t.Fatalf("%s is a container with no declared entries property", d.ID) + t.Fatalf("%s is a container with no declared %q property", d.ID, d.Container.Count) return 0 } diff --git a/internal/guard/contains_test.go b/internal/guard/contains_test.go index 1bb8ef34..b1f817d4 100644 --- a/internal/guard/contains_test.go +++ b/internal/guard/contains_test.go @@ -262,7 +262,7 @@ func TestContentsThatCannotBeHonouredAreRefusedWithTheRightCode(t *testing.T) { name: "contents stated twice", body: " - id: a\n format: zip\n contains: [{format: pdf, count: 1, size: 4kb}]\n properties: {entries: 2}\n", code: cli.ExitRecipe, - says: "both say what the archive holds", + says: "both say which files it holds", }, { name: "a group with no size", diff --git a/internal/guard/eml_test.go b/internal/guard/eml_test.go new file mode 100644 index 00000000..4e2858f0 --- /dev/null +++ b/internal/guard/eml_test.go @@ -0,0 +1,245 @@ +package guard + +import ( + "bytes" + "context" + "errors" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync/atomic" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/eml" + "github.com/donislawdev/TestingFilesGenerator/internal/oracle" +) + +// emlCases is every value of every setting, one at a time, then the +// combinations where the settings meet: names outside ASCII in every style, +// both line endings under quoted-printable, and alternative with all of it. +var emlCases = []map[string]string{ + {}, + {eml.Body: eml.HTML}, + {eml.Body: eml.Alternative}, + {eml.TextEncoding: eml.QuotedPrintable}, + {eml.TextEncoding: eml.EightBit}, + {eml.Headers: eml.Encoded}, + {eml.Headers: eml.UTF8}, + {eml.LineEndings: eml.LF}, + {eml.FilenameStyle: eml.RFC2047}, + {eml.FilenameStyle: eml.Both}, + {eml.FilenameStyle: eml.ContentType}, + {eml.Headers: eml.Encoded, eml.FilenameStyle: eml.RFC2047}, + {eml.Headers: eml.Encoded, eml.FilenameStyle: eml.Both}, + {eml.Headers: eml.Encoded, eml.FilenameStyle: eml.ContentType}, + {eml.Headers: eml.UTF8, eml.FilenameStyle: eml.Both, eml.LineEndings: eml.LF}, + {eml.TextEncoding: eml.QuotedPrintable, eml.LineEndings: eml.LF, eml.Body: eml.HTML}, + {eml.Body: eml.Alternative, eml.TextEncoding: eml.QuotedPrintable, eml.Headers: eml.Encoded, + eml.FilenameStyle: eml.Both, eml.LineEndings: eml.LF}, + {eml.Body: eml.Alternative, eml.TextEncoding: eml.EightBit, eml.Headers: eml.UTF8, eml.FilenameStyle: eml.RFC2047}, +} + +// Three readers and a scanner, each asked what the others cannot answer +// (docs/EML-2026-10-07.md section 5.1). Python's email package has to read +// every message with no defect but the one headers=utf8 is measured to bring. +// mailparser, where it is installed, has to read the same attached files under +// the same names. The structural checker is told the settings and has to find +// the message made the way they say - the header lines RFC 5322 requires, the +// lines, the encodings, where each name is written. Go's standard library has +// its own guard beside this one. +// +// Every size from the smallest the settings allow, where the label does not fit +// and the text is empty, through the next one, to a message with room for +// words. Named to match ReferenceTool, because that is the one job in CI with +// mailparser installed. Anywhere without python it skips, and says so. +func TestEveryEMLSettingSurvivesItsReferenceTool(t *testing.T) { + d, err := format.Get("eml") + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + var parsed, byNode atomic.Int64 + // Each case in a subtest of its own, run side by side: every message is a + // file of its own and every reader a process of its own, and one after + // another the 144 messages kept the job waiting on interpreters starting + // (review of #173). The group returns when every case has. + t.Run("cases", func(t *testing.T) { + for i, base := range emlCases { + for _, count := range []string{"0", "2"} { + t.Run(strconv.Itoa(i)+"_"+count, func(t *testing.T) { + t.Parallel() + props := withAttachments(base, count, "txt", "1kb") + smallest := d.SmallestAccepted(format.Request{Properties: props}) + for _, size := range []int64{smallest, smallest + 1, smallest + 2, 20000} { + path := filepath.Join(dir, strconv.Itoa(i)+"_"+count+"_"+strconv.FormatInt(size, 10)+".eml") + b, _ := writeEML(t, d, size, uint64(31+i), props) + if err := os.WriteFile(path, b, 0o600); err != nil { + t.Fatal(err) + } + py, node := emlSurvivesReaders(t, path, props, size) + parsed.Add(int64(py)) + byNode.Add(int64(node)) + } + }) + } + } + }) + // Where mailparser is named it has to have read something, or the step + // that installed it and this guard disagree about where it is. + if os.Getenv("TFG_MAILPARSER") != "" && byNode.Load() == 0 { + t.Errorf("TFG_MAILPARSER names %q and mailparser read no message", os.Getenv("TFG_MAILPARSER")) + } + t.Logf("%d messages read by python, %d by mailparser", parsed.Load(), byNode.Load()) + if parsed.Load() == 0 { + t.Skip("python is not installed, so no message was read - a skip, not a pass") + } +} + +// emlSurvivesReaders asks the readers about one message and says which of +// python and mailparser read it. +func emlSurvivesReaders(t *testing.T, path string, props map[string]string, size int64) (python, node int) { + t.Helper() + var allowed []string + if props[eml.Headers] == eml.UTF8 { + allowed = append(allowed, "from:UndecodableBytesDefect", "to:UndecodableBytesDefect") + } + py := oracle.PythonEmail(path, allowed...) + if !py.Available { + return 0, 0 + } + if py.Err != nil { + t.Fatalf("%v at %d B: %v", props, size, py.Err) + } + mp := oracle.MailParser(path) + if mp.Available && mp.Err != nil { + t.Fatalf("%v at %d B: %v", props, size, mp.Err) + } else if mp.Available && attachmentsOf(mp.Output) != attachmentsOf(py.Output) { + t.Fatalf("%v at %d B: mailparser and python read different files\n mailparser %s\n python %s", + props, size, attachmentsOf(mp.Output), attachmentsOf(py.Output)) + } + if s := oracle.Strict("eml", path, emlSettings(props)...); s.Err != nil { + t.Fatalf("%v at %d B: %v", props, size, s.Err) + } + return 1, boolInt(mp.Available) +} + +// attachmentsOf is the list of attached files a reader printed, for comparing +// two readers - the subject is left out, because each prints it its own way. +func attachmentsOf(output string) string { + i := strings.Index(output, `"attachments"`) + if i < 0 { + return output + } + return strings.ReplaceAll(output[i:], " ", "") +} + +// A message too small for its label goes without one and says so - the note +// and label_embedded, both - rather than being refused or rounded. The size is +// taken from between the two minimums, so the guard is in the state it is +// about: it asserts that the bare message fits and the labelled one does not. +func TestAnEMLTooSmallForItsLabelSaysItCarriesNone(t *testing.T) { + d, err := format.Get("eml") + if err != nil { + t.Fatal(err) + } + bare := d.SmallestAccepted(format.Request{}) + _, labelled := writeEML(t, d, bare+200, 5, nil) + if labelled.Properties[format.PropertyLabelEmbedded] != true { + t.Fatalf("a message of %d B carries no label, so this guard is not between the two minimums", bare+200) + } + p, err := d.Generator.Plan(format.Request{Bytes: bare, Seed: 5, Label: true}) + if err != nil { + t.Fatalf("the smallest message with a label asked for was refused: %v", err) + } + if p.Properties[format.PropertyLabelEmbedded] != false || len(p.Notes) != 1 || p.Notes[0].Code != "label_omitted" { + t.Errorf("a message of %d B with no room for its label says label_embedded=%v and notes %v", + bare, p.Properties[format.PropertyLabelEmbedded], p.Notes) + } +} + +// Below the smallest message the settings allow is refused with the four +// parts every refusal of size has (D6), and with the reason a tester needs: +// the files travel in base64, a third larger. A message inside a message is +// refused as nesting, the way an archive inside an archive is. +func TestEMLRefusesASizeBelowItsAttachmentsInBase64AndItselfInside(t *testing.T) { + d, err := format.Get("eml") + if err != nil { + t.Fatal(err) + } + props := withAttachments(nil, "3", "txt", "64kb") + _, err = d.Generator.Plan(format.Request{Bytes: 3 * 64 << 10, Seed: 1, Properties: props}) + var below *format.BelowMinimumError + if !errors.As(err, &below) { + t.Fatalf("three files of 64 KiB in a message of 192 KiB were not refused as too small: %v", err) + } + if below.Minimum <= 3*64<<10*4/3 || !strings.Contains(below.Reason.String(), "base64") { + t.Errorf("the refusal gives the minimum %d B and the reason %q", below.Minimum, below.Reason) + } + _, err = d.Generator.Plan(format.Request{Bytes: 1 << 20, Seed: 1, Properties: withAttachments(nil, "1", "eml", "4kb")}) + var nested *format.NestingUnsupportedError + if !errors.As(err, &nested) { + t.Errorf("a message attached to a message was not refused as nesting: %v", err) + } +} + +// alternative carries the same words in both parts, so its text grows two +// bytes at a time, and an odd byte left over becomes one space in the HTML, +// where it changes nothing a person sees. Two neighbouring sizes have to land +// in both states - asserted rather than assumed, because a golden case named +// for the odd byte was first pinned at a size that left none (2026-10-07). +func TestAlternativePutsTheOddByteInTheHTML(t *testing.T) { + d, err := format.Get("eml") + if err != nil { + t.Fatal(err) + } + reached := map[bool]bool{} + for size := int64(4000); size < 4002; size++ { + b, _ := writeEML(t, d, size, 9, map[string]string{eml.Body: eml.Alternative}) + reached[bytes.Contains(b, []byte(" "))] = true + } + if !reached[true] || !reached[false] { + t.Errorf("two neighbouring sizes did not reach both an even and an odd byte: %v", reached) + } +} + +// withAttachments is a case with the attachment settings added. +func withAttachments(base map[string]string, count, id, size string) map[string]string { + out := map[string]string{eml.Attachments: count, eml.AttachmentFormat: id, eml.AttachmentSize: size} + for k, v := range base { + out[k] = v + } + return out +} + +// writeEML plans and writes one message, and checks it is the size ordered. +func writeEML(t *testing.T, d format.Descriptor, size int64, seed uint64, props map[string]string) ([]byte, format.Plan) { + t.Helper() + p, err := d.Generator.Plan(format.Request{Bytes: size, Seed: seed, Label: true, Properties: props}) + if err != nil { + t.Fatalf("planning %d B with %v: %v", size, props, err) + } + var buf bytes.Buffer + if err := d.Generator.Write(context.Background(), &buf, p); err != nil { + t.Fatalf("writing %d B with %v: %v", size, props, err) + } + if int64(buf.Len()) != size { + t.Fatalf("%v: asked for %d B and got %d", props, size, buf.Len()) + } + return buf.Bytes(), p +} + +// emlSettings is a case as the words the structural checker is told. +func emlSettings(props map[string]string) []string { + out := make([]string, 0, len(props)) + for k, v := range props { + if k == eml.AttachmentFormat || k == eml.AttachmentSize { + continue + } + out = append(out, k+"="+v) + } + sort.Strings(out) + return out +} diff --git a/internal/guard/emlgo_test.go b/internal/guard/emlgo_test.go new file mode 100644 index 00000000..82ceaba3 --- /dev/null +++ b/internal/guard/emlgo_test.go @@ -0,0 +1,216 @@ +package guard + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "mime" + "mime/multipart" + "net/mail" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/eml" +) + +// Go's standard library is the fourth reader of a mail message, and the one +// every service written in Go meets without a mail library: net/mail for the +// message, mime/multipart for the parts, Part.FileName for a name. Measured +// 2026-10-07 (docs/EML-2026-10-07.md section 5.1), it is the strictest of the +// four on structure - the only one that refused a message cut short, one with +// no closing boundary and one with a stray character in base64 - and the only +// one that disagrees about names: it reads none from Content-Type, and it hands +// back an RFC 2047 name inside quotes as the encoded text itself. The owner's +// decision of the same day put it in the guards, where it costs nothing to run. +// +// It runs in this process, so it needs no tool installed and runs everywhere. +// net/mail is linked into the guards only - the command line links no network +// package at all (D16), and a guard checks that. + +// goPart is one leaf of a message as the standard library reads it. +type goPart struct { + mediaType string + name string + attached bool + data []byte +} + +// goReadMail reads a message the way a Go service does and returns its leaves. +func goReadMail(b []byte) ([]goPart, *mail.Message, error) { + msg, err := mail.ReadMessage(bytes.NewReader(b)) + if err != nil { + return nil, nil, fmt.Errorf("ReadMessage: %w", err) + } + var out []goPart + err = goWalk(&out, msg.Header.Get("Content-Type"), msg.Header.Get("Content-Transfer-Encoding"), "", "", msg.Body) + return out, msg, err +} + +// goWalk reads one entity, going down into a multipart one - alternative +// inside mixed is the shape eml writes. +func goWalk(out *[]goPart, contentType, transfer, disposition, name string, body io.Reader) error { + mediaType, params, err := mime.ParseMediaType(contentType) + if err != nil { + return fmt.Errorf("ParseMediaType %q: %w", contentType, err) + } + if strings.HasPrefix(mediaType, "multipart/") { + mr := multipart.NewReader(body, params["boundary"]) + for { + p, err := mr.NextPart() + if err == io.EOF { + return nil + } + if err != nil { + return fmt.Errorf("NextPart: %w", err) + } + if err := goWalk(out, p.Header.Get("Content-Type"), p.Header.Get("Content-Transfer-Encoding"), + p.Header.Get("Content-Disposition"), p.FileName(), p); err != nil { + return err + } + } + } + // A part reading quoted-printable decodes it itself and drops the header, + // so only base64 is left to undo here. + if strings.EqualFold(transfer, "base64") { + body = base64.NewDecoder(base64.StdEncoding, body) + } + data, err := io.ReadAll(body) + if err != nil { + return fmt.Errorf("reading a %s part: %w", mediaType, err) + } + *out = append(*out, goPart{mediaType: mediaType, name: name, attached: strings.HasPrefix(disposition, "attachment"), data: data}) + return nil +} + +// goNameFor is the name the standard library is measured to read for an +// attached file - the one each setting writes, but for the two places it was +// measured to read something else. +func goNameFor(props map[string]string, written string) string { + switch { + case props[eml.FilenameStyle] == eml.ContentType: + return "" + case props[eml.FilenameStyle] == eml.RFC2047 && props[eml.Headers] == eml.Encoded: + return "=?UTF-8?B?" + } + return written +} + +// TestGosStandardLibraryReadsEveryEMLTheWayItWasMeasured reads a message of +// every setting with the standard library and asks for three things: every +// attached file comes back byte for byte the file its own format writes with +// the seed it was given (RC4), every name is the one the settings write - or +// exactly the other thing this reader was measured to read - and the text +// parts are the ones body orders. +// +// The name of each file is asked of a setting that makes it plain to read, so +// the guard asserts that the disagreement is still there rather than assuming +// it: a reader that started decoding rfc2047 would turn this red, and the +// sentence beside filename_style would then be wrong. +func TestGosStandardLibraryReadsEveryEMLTheWayItWasMeasured(t *testing.T) { + d, err := format.Get("eml") + if err != nil { + t.Fatal(err) + } + disagreed := 0 + for i, props := range emlCases { + props = withAttachments(props, "2", "pdf", "4kb") + b, plan := writeEML(t, d, 40000+int64(i)*101, uint64(70+i), props) + parts, _, err := goReadMail(b) + if err != nil { + t.Fatalf("%v: the standard library refused the message: %v", props, err) + } + var files []goPart + var texts []string + for _, p := range parts { + if p.attached { + files = append(files, p) + } else { + texts = append(texts, p.mediaType) + } + } + if want := textsFor(props[eml.Body]); strings.Join(texts, " ") != want { + t.Errorf("%v: the text parts are %v, and body orders %s", props, texts, want) + } + if len(files) != 2 { + t.Fatalf("%v: %d attached files read, 2 ordered", props, len(files)) + } + for n, f := range files { + if want := childBytes(t, "pdf", 4<<10, core.FileSeed(uint64(70+i), n)); !bytes.Equal(f.data, want) { + t.Errorf("%v: attached file %d is not the pdf its seed makes (%d B against %d B)", props, n+1, len(f.data), len(want)) + } + written := emlName(props, "pdf", n+1, ".pdf") + want := goNameFor(props, written) + if (want == "=?UTF-8?B?" && strings.HasPrefix(f.name, want)) || f.name == want { + disagreed += boolInt(want != written) + continue + } + t.Errorf("%v: the standard library read the name %q, and %q was measured", props, f.name, want) + } + if got := plan.Properties["mime_entities"]; got != len(parts)+entitiesAbove(props) { + t.Errorf("%v: the manifest counts %v MIME entities and the reader found %d leaves", props, got, len(parts)) + } + } + if disagreed == 0 { + t.Error("no case reached a name the standard library reads otherwise, so the measured disagreement was not asked about") + } +} + +// textsFor is the text parts body orders, as their media types in order. +func textsFor(body string) string { + switch body { + case eml.HTML: + return "text/html" + case eml.Alternative: + return "text/plain text/html" + } + return "text/plain" +} + +// entitiesAbove is how many entities hold the leaves: the message itself as +// multipart/mixed, and multipart/alternative when body orders it. +func entitiesAbove(props map[string]string) int { + if props[eml.Body] == eml.Alternative { + return 2 + } + return 1 +} + +// emlName is the name the generator gives an attached file, spelled here from +// the rule rather than read from the generator, so the two can disagree. +func emlName(props map[string]string, id string, n int, ext string) string { + name := fmt.Sprintf("%s_%04d%s", id, n, ext) + if props[eml.Headers] == eml.Encoded || props[eml.Headers] == eml.UTF8 { + name = "Załącznik_日本語_" + name + } + return name +} + +// childBytes is the file a format writes for a size and a seed, as the message +// is meant to carry it. +func childBytes(t *testing.T, id string, size int64, seed uint64) []byte { + t.Helper() + d, err := format.Get(id) + if err != nil { + t.Fatal(err) + } + p, err := d.Generator.Plan(format.Request{Bytes: size, Seed: seed, Label: true}) + if err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + if err := d.Generator.Write(context.Background(), &buf, p); err != nil { + t.Fatal(err) + } + return buf.Bytes() +} + +func boolInt(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/internal/guard/generatorbytes_test.go b/internal/guard/generatorbytes_test.go index 869d63a4..9d816252 100644 --- a/internal/guard/generatorbytes_test.go +++ b/internal/guard/generatorbytes_test.go @@ -42,7 +42,7 @@ const generatorGoldenFile = "testdata/generator-golden.json" const goldenSeed = 7741 func goldenCases() map[string]engine.Target { - return map[string]engine.Target{ + cases := map[string]engine.Target{ // Every format at a size comfortably above its minimum. "txt_4kib": {ID: "g", Format: "txt", Sizes: engine.Uniform(1, 4096), Label: true}, "png_64kib": {ID: "g", Format: "png", Sizes: engine.Uniform(1, 65536), Label: true, @@ -352,36 +352,6 @@ func goldenCases() map[string]engine.Target { "toml_8kib": {ID: "g", Format: "toml", Sizes: engine.Uniform(1, 8192), Label: true}, "toml_8kib_no_label": {ID: "g", Format: "toml", Sizes: engine.Uniform(1, 8192), Label: false}, - // GeoJSON, one case for each path the bytes take rather than one for - // each setting: the three layouts, whole degrees (no decimal point at - // all), every pitfall at once with fifteen places - the wrap across the - // antimeridian, the reversed outline and the height whose span passes - // an int64 - and a note capped at a mebibyte with spaces after it. The - // label never reaches the content, so it is pinned in one position. - "geojson_8kib": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true}, - "geojson_8kib_minified": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"formatting": "minified"}}, - "geojson_8kib_indented": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"formatting": "indented"}}, - "geojson_8kib_whole_degrees": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"precision": "0"}}, - "geojson_8kib_every_pitfall": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, - Properties: map[string]string{"antimeridian": "true", "winding": "reversed", "altitude": "true", "precision": "15"}}, - "geojson_capped_note": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 3900000), Label: true, - Properties: map[string]string{"geometry": "polygon", "vertices": "100000"}}, - // The second set of settings, one case for each new path: outlines - // with holes, a box on every feature and on the collection across the - // antimeridian with a height and indented, features with no place and - // ids of both types, and a collection box that goes all the way round - - // each read by the structural checker when its value was recorded, the - // last two reporting a crossing box and a box round the globe. - "geojson_8kib_holes": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, - Properties: map[string]string{"holes": "2"}}, - "geojson_8kib_boxes_across": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, - Properties: map[string]string{"bbox": "true", "antimeridian": "true", "altitude": "true", "formatting": "indented"}}, - "geojson_8kib_unlocated_mixed_ids": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, - Properties: map[string]string{"unlocated": "some", "ids": "mixed"}}, - "geojson_box_round_the_globe": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 65536), Label: true, - Properties: map[string]string{"bbox": "true", "antimeridian": "true", "precision": "0", "vertices": "300", - "geometry": "linestring", "formatting": "minified", "ids": "string"}}, - // The label is a byte affecting switch, not a cosmetic one, so it is // pinned in both positions. "txt_4kib_no_label": {ID: "g", Format: "txt", Sizes: engine.Uniform(1, 4096), Label: false}, @@ -425,6 +395,70 @@ func goldenCases() map[string]engine.Target { // have to agree to the byte. "targz_past_the_comment_limit": {ID: "g", Format: "targz", Sizes: engine.Uniform(1, 262144), Label: true}, } + for _, more := range []map[string]engine.Target{mapGoldenCases(), mailGoldenCases()} { + for name, target := range more { + cases[name] = target + } + } + return cases +} + +// mapGoldenCases are the golden cases of the geojson format, a table of their +// own for the reason mailGoldenCases gives. +func mapGoldenCases() map[string]engine.Target { + return map[string]engine.Target{ + // GeoJSON, one case for each path the bytes take rather than one for + // each setting: the three layouts, whole degrees (no decimal point at + // all), every pitfall at once with fifteen places - the wrap across the + // antimeridian, the reversed outline and the height whose span passes + // an int64 - and a note capped at a mebibyte with spaces after it. The + // label never reaches the content, so it is pinned in one position. + "geojson_8kib": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true}, + "geojson_8kib_minified": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"formatting": "minified"}}, + "geojson_8kib_indented": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"formatting": "indented"}}, + "geojson_8kib_whole_degrees": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"precision": "0"}}, + "geojson_8kib_every_pitfall": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, + Properties: map[string]string{"antimeridian": "true", "winding": "reversed", "altitude": "true", "precision": "15"}}, + "geojson_capped_note": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 3900000), Label: true, + Properties: map[string]string{"geometry": "polygon", "vertices": "100000"}}, + + // The second set of settings, one case for each new path: outlines + // with holes, a box on every feature and on the collection across the + // antimeridian with a height and indented, features with no place and + // ids of both types, and a collection box that goes all the way round - + // each read by the structural checker when its value was recorded, the + // last two reporting a crossing box and a box round the globe. + "geojson_8kib_holes": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, + Properties: map[string]string{"holes": "2"}}, + "geojson_8kib_boxes_across": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, + Properties: map[string]string{"bbox": "true", "antimeridian": "true", "altitude": "true", "formatting": "indented"}}, + "geojson_8kib_unlocated_mixed_ids": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 8192), Label: true, + Properties: map[string]string{"unlocated": "some", "ids": "mixed"}}, + "geojson_box_round_the_globe": {ID: "g", Format: "geojson", Sizes: engine.Uniform(1, 65536), Label: true, + Properties: map[string]string{"bbox": "true", "antimeridian": "true", "precision": "0", "vertices": "300", + "geometry": "linestring", "formatting": "minified", "ids": "string"}}, + } +} + +// mailGoldenCases are the golden cases of the eml format, a table of their own +// so goldenCases stays a length a person can read. +func mailGoldenCases() map[string]engine.Target { + return map[string]engine.Target{ + // One case for each path the bytes take: the plain default, HTML in + // quoted-printable, alternative at a size that leaves an odd byte (the + // space the HTML takes for what the two parts cannot split) with raw UTF-8 headers and + // LF, and attached files under every style of name - encoded words, the + // RFC 2231 form continued over lines, and the name in Content-Type only. + "eml_8kib": {ID: "m", Format: "eml", Sizes: engine.Uniform(1, 8192), Label: true}, + "eml_8kib_html_quoted": {ID: "m", Format: "eml", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"body": "html", "text_encoding": "quoted-printable"}}, + "eml_8kib_alternative_odd_utf8_lf": {ID: "m", Format: "eml", Sizes: engine.Uniform(1, 8192), Label: true, Properties: map[string]string{"body": "alternative", "text_encoding": "8bit", "headers": "utf8", "line_endings": "lf"}}, + "eml_64kib_attached_both": {ID: "m", Format: "eml", Sizes: engine.Uniform(1, 65536), Label: true, + Properties: map[string]string{"attachments": "2", "attachment_format": "txt", "attachment_size": "8kb", "headers": "encoded", "filename_style": "both"}}, + "eml_64kib_attached_rfc2047": {ID: "m", Format: "eml", Sizes: engine.Uniform(1, 65536), Label: true, + Properties: map[string]string{"attachments": "1", "attachment_format": "pdf", "attachment_size": "8kb", "headers": "encoded", "filename_style": "rfc2047"}}, + "eml_64kib_attached_content_type": {ID: "m", Format: "eml", Sizes: engine.Uniform(1, 65536), Label: true, + Properties: map[string]string{"attachments": "3", "attachment_format": "png", "attachment_size": "4kb", "filename_style": "content-type"}}, + } } // TestEveryFormatHasAGoldenValue asks whether the pinning below covers what diff --git a/internal/guard/layers_test.go b/internal/guard/layers_test.go index 52269b0a..436e2444 100644 --- a/internal/guard/layers_test.go +++ b/internal/guard/layers_test.go @@ -49,6 +49,7 @@ var layer = map[string]int{ "internal/format/csvfile": 1, "internal/format/jsonfile": 1, "internal/format/geojson": 1, + "internal/format/eml": 1, "internal/format/yamlfile": 1, "internal/format/tomlfile": 1, "internal/format/xmlfile": 1, @@ -155,6 +156,7 @@ var sameLayerAllowed = map[string][]string{ "internal/format/wav", "internal/format/webm", "internal/format/mp4", + "internal/format/eml", }, "internal/format/imagelabel": {"internal/format"}, "internal/format/imagedim": {"internal/format"}, @@ -183,6 +185,7 @@ var sameLayerAllowed = map[string][]string{ "internal/format/pdf": {"internal/format", "internal/format/imagelabel"}, "internal/format/zip": {"internal/format", "internal/format/imagelabel", "internal/format/archive"}, "internal/format/targz": {"internal/format", "internal/format/archive"}, + "internal/format/eml": {"internal/format", "internal/format/archive"}, "internal/format/tiff": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/webp": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/avif": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim", "internal/format/isobmff"}, diff --git a/internal/guard/oracle_test.go b/internal/guard/oracle_test.go index 01b7d3fa..7b061247 100644 --- a/internal/guard/oracle_test.go +++ b/internal/guard/oracle_test.go @@ -181,6 +181,11 @@ var structurallyChecked = map[string]bool{ // layer knows how many decimal places and which winding were ordered // (docs/GEOJSON-2026-10-07.md section 3). "geojson": true, + // Since 2026-10-07, from its first day: four mail readers took a message + // with no From line and a header line of 2000 characters without a word, + // and only this layer knows where each name was ordered to be written + // (docs/EML-2026-10-07.md section 5.1). + "eml": true, } func TestTheStructuralCheckerCoversEveryFormatItShould(t *testing.T) { diff --git a/internal/guard/parity_test.go b/internal/guard/parity_test.go index 4aa73520..8acec4c4 100644 --- a/internal/guard/parity_test.go +++ b/internal/guard/parity_test.go @@ -128,6 +128,7 @@ var reachableFromTheWindow = []string{ "format:pptx", "format:json", "format:geojson", + "format:eml", "format:log", "format:avif", "format:jxl", @@ -161,6 +162,14 @@ var reachableFromTheWindow = []string{ "property:csv.line_ending", "property:csv.quote_style", "property:docx.paragraphs", + "property:eml.attachment_format", + "property:eml.attachment_size", + "property:eml.attachments", + "property:eml.body", + "property:eml.filename_style", + "property:eml.headers", + "property:eml.line_endings", + "property:eml.text_encoding", "property:pptx.slides", "property:xlsx.columns", "property:xlsx.rows", diff --git a/internal/guard/presetbytes_test.go b/internal/guard/presetbytes_test.go index a1b16eea..fc5474ae 100644 --- a/internal/guard/presetbytes_test.go +++ b/internal/guard/presetbytes_test.go @@ -94,7 +94,10 @@ func TestEjectingAPresetGivesTheBytesItAlwaysGave(t *testing.T) { // Moved a fifth time on 2026-10-07, under the same decision: geojson is // a format more, so the set has its smallest collection, 875 B // (docs/GEOJSON-2026-10-07.md). - {id: "empty-and-minimal", args: preset.Args{}, bytes: 9885, sum: "2a02ad1ec2e2c2ff7f19b87906ecf3ca910e1af5e806b135467824c28fb4fc24"}, + // Moved a sixth time on 2026-10-07, under the same decision: eml is a + // format more, so the set has its smallest message, 289 B + // (docs/EML-2026-10-07.md). + {id: "empty-and-minimal", args: preset.Args{}, bytes: 10190, sum: "9530e0e5427d2d92cf05f47a5dcd4fc1755d67a9155ea65482e296f4d571adbe"}, {id: "empty-and-minimal", args: preset.Args{"formats": "jpg,png,txt"}, bytes: 1472, sum: "19b12800372395ad3bf5cba6407ca654d7ed51eba3a5bc5436f76a94bc09253c"}, // The preset of unusual file names, measured 2026-09-25 on its first // build: the default, and a format whose extension is a byte longer, diff --git a/internal/guard/privatecontent_test.go b/internal/guard/privatecontent_test.go index 644a06af..901c7aa0 100644 --- a/internal/guard/privatecontent_test.go +++ b/internal/guard/privatecontent_test.go @@ -57,6 +57,13 @@ var privatePatterns = []struct { // fixture ever carries one the remedy is to use an address from here. var documentationNets = regexp.MustCompile(`\b(?:192\.0\.2\.|198\.51\.100\.|203\.0\.113\.)`) +// documentationMail is the mail counterpart: the three names RFC 2606 keeps for +// examples, which no mailbox is at. The eml format writes its messages between +// two of them on purpose, so a message that leaves a test reaches nobody. Only +// these three - a made up name under the reserved .example domain is still +// reported, because a guard below asks for exactly that. +var documentationMail = regexp.MustCompile(`(?i)@example\.(?:com|org|net)$`) + // homePlaceholders are the names an example uses when it means "your home // directory". Refusing those would refuse ordinary help text. var homePlaceholders = map[string]bool{ @@ -74,6 +81,8 @@ func privateFaults(text string) []string { continue case p.name == "an address on somebody's private network" && documentationNets.MatchString(m[0]): continue + case p.name == "an e-mail address" && documentationMail.MatchString(m[0]): + continue // A no-reply address names a service rather than a person, and // this project's own commit trailers carry one. case p.name == "an e-mail address" && strings.Contains(strings.ToLower(m[0]), "noreply"): @@ -115,7 +124,7 @@ func TestTheScanForPrivateContentCanActuallyFindIt(t *testing.T) { // clearest evidence it works. "10.1." + "2.3", "192.168." + "0.4", - "somebody" + "@" + "example.org", + "somebody" + "@" + "customer.example", "-----" + "BEGIN " + "SOMETHING", "AKIA" + "0123456789ABCDEF", "ghp_" + strings.Repeat("a", 36), @@ -143,6 +152,7 @@ func TestTheScanForPrivateContentLeavesOrdinaryTextAlone(t *testing.T) { "192.0.2.1", "203.0.113.9", "noreply" + "@" + "anthropic.com", + "alice" + "@" + "example.com", "invoice.txt", }, "\n") diff --git a/internal/guard/privatepng_test.go b/internal/guard/privatepng_test.go index e92d2150..448ec4a0 100644 --- a/internal/guard/privatepng_test.go +++ b/internal/guard/privatepng_test.go @@ -227,7 +227,7 @@ func zTXtChunk(keyword string, text []byte) []byte { // a PNG's text chunk is still found - plain, and behind zlib. Without this the // narrowing to text chunks could quietly become "pictures are never read". func TestAnAddressInAPictureTextChunkIsStillFound(t *testing.T) { - address := strings.Join([]string{"someone", "example.org"}, "@") + address := strings.Join([]string{"someone", "customer.example"}, "@") for name, png := range map[string][]byte{ "tEXt": pngWithText("tEXt", append([]byte("Author\x00exported by "), address...)), "zTXt": pngWithText("zTXt", zTXtChunk("Author", []byte("exported by "+address))), diff --git a/internal/guard/registry_test.go b/internal/guard/registry_test.go index 84540cdf..f9cc371d 100644 --- a/internal/guard/registry_test.go +++ b/internal/guard/registry_test.go @@ -36,6 +36,8 @@ func TestEveryFormatDeclaresTheFullSet(t *testing.T) { if !strings.HasPrefix(d.Extension, ".") { t.Errorf("extension %q does not start with a dot", d.Extension) } + mediaTypeIsSpelledOut(t, d.MediaType) + membersAreDeclared(t, d) if d.Generator == nil { t.Error("no generator") } @@ -121,6 +123,42 @@ func nameShowsAsItIs(t *testing.T, name string) { } } +// mediaTypeShape is a type and a subtype in lower case, the way the IANA +// registry lists them. Lower case because a type is compared without case and +// written in one, and a mail message carrying Application/PDF beside every +// other sender's application/pdf is a difference a test would trip over. +var mediaTypeShape = regexp.MustCompile(`^(application|audio|font|image|message|model|text|video)/[a-z0-9][a-z0-9.+-]*$`) + +// mediaTypeIsSpelledOut asks for the media type a mail message puts on an +// attachment of this format. Empty is the failure that matters: a message would +// carry the file under no type, and nothing else would notice. +func mediaTypeIsSpelledOut(t *testing.T, mediaType string) { + t.Helper() + if !mediaTypeShape.MatchString(mediaType) { + t.Errorf("the media type %q is not a type and a subtype in lower case, such as application/pdf", mediaType) + } +} + +// membersAreDeclared asks a container for the three settings that order its +// contents in one group, each one declared as a setting it takes, and asks +// every other format for none. A container naming a key it does not declare +// would refuse its own short way of being asked. +func membersAreDeclared(t *testing.T, d format.Descriptor) { + t.Helper() + if d.Container == nil { + return + } + declared := map[string]bool{} + for _, p := range d.Properties { + declared[p.Name] = true + } + for _, k := range []string{d.Container.Count, d.Container.Format, d.Container.Size} { + if !declared[k] { + t.Errorf("is a container whose members setting %q is not a setting it declares", k) + } + } +} + // The registry is the source of the confirmed minimum and the format document // keeps an approximate table beside it. This is what stops the two drifting. // diff --git a/internal/guard/resources_test.go b/internal/guard/resources_test.go index 3be2f4ca..cef47184 100644 --- a/internal/guard/resources_test.go +++ b/internal/guard/resources_test.go @@ -113,8 +113,8 @@ func growthAllowanceFor(steady int64) int64 { // growth check below is untouched and applies to every format equally, and it // is the one that asks the question this ceiling is a proxy for. func ceilingFor(d format.Descriptor) int64 { - if d.AllocCeiling > 0 { - return d.AllocCeiling + if a, ok := d.Generator.(format.AllocCeiling); ok { + return a.AllocCeiling() } return allocCeiling } diff --git a/internal/guard/site_test.go b/internal/guard/site_test.go index e474cc8a..73464f77 100644 --- a/internal/guard/site_test.go +++ b/internal/guard/site_test.go @@ -113,7 +113,7 @@ func factsFromTheProgram(t *testing.T) site.Facts { Smallest: d.SmallestAccepted(format.Request{Label: true}), Padding: d.Padding.Name, Oracle: d.Oracle, - Container: d.Container, + Container: d.Container != nil, Properties: props, }) } diff --git a/internal/guard/sizeceiling_test.go b/internal/guard/sizeceiling_test.go index e5616805..4e0b23dc 100644 --- a/internal/guard/sizeceiling_test.go +++ b/internal/guard/sizeceiling_test.go @@ -51,8 +51,8 @@ func TestAFormatRefusesASizeItCannotDescribe(t *testing.T) { for _, d := range format.All() { for _, s := range sizes { req := format.Request{Bytes: s.bytes, Seed: 7, Label: true} - if d.Container { - req.Properties = map[string]string{"entries": "1"} + if d.Container != nil { + req.Properties = map[string]string{d.Container.Count: "1"} } plan, err := d.Generator.Plan(req) @@ -144,8 +144,8 @@ func TestARefusalAboutTooLargeSaysLargerNotSmaller(t *testing.T) { found := 0 for _, d := range format.All() { req := format.Request{Bytes: 1 << 40, Seed: 7, Label: true} - if d.Container { - req.Properties = map[string]string{"entries": "1"} + if d.Container != nil { + req.Properties = map[string]string{d.Container.Count: "1"} } _, err := d.Generator.Plan(req) if err == nil { @@ -214,8 +214,8 @@ func TestAContainerRefusesASizeItsArithmeticCannotSee(t *testing.T) { containers++ req := format.Request{Bytes: 5 << 30, Seed: 7, Label: true} - if d.Container { - req.Properties = map[string]string{"entries": "1"} + if d.Container != nil { + req.Properties = map[string]string{d.Container.Count: "1"} } _, err := d.Generator.Plan(req) if err == nil { diff --git a/internal/guard/testdata/generator-golden.json b/internal/guard/testdata/generator-golden.json index e9f4b85b..9ecb169e 100644 --- a/internal/guard/testdata/generator-golden.json +++ b/internal/guard/testdata/generator-golden.json @@ -431,6 +431,36 @@ "bytes": 65536, "sha256": "5c01a4d2461685ed35c3e431da8452742d241e54f7b9e9d9158e87694e70a1dc", "measured_on": "2026-10-07" + }, + "eml_8kib": { + "bytes": 8192, + "sha256": "cd441a55e0fad36d2b6408cdbbaa5813061649ad7436b3bc6cbec57250e995f1", + "measured_on": "2026-10-07" + }, + "eml_8kib_html_quoted": { + "bytes": 8192, + "sha256": "f6d591ecbacca1fe33b7213419939ebd6f72a66d01605358c4885ad1d561b771", + "measured_on": "2026-10-07" + }, + "eml_8kib_alternative_odd_utf8_lf": { + "bytes": 8192, + "sha256": "aee420e64729d5fe3b60163040c464fe3a5489ea650e168252f4abd2c84bb81f", + "measured_on": "2026-10-07" + }, + "eml_64kib_attached_both": { + "bytes": 65536, + "sha256": "f5fd01b6a934e0956d25aef31727e792948b693f398a1af0f47cd9a9fda71133", + "measured_on": "2026-10-07" + }, + "eml_64kib_attached_rfc2047": { + "bytes": 65536, + "sha256": "0fad092a621a172e67f01347418aa401e1a94b6ed40da8e7a9032a09b5beaa71", + "measured_on": "2026-10-07" + }, + "eml_64kib_attached_content_type": { + "bytes": 65536, + "sha256": "425f762165582768bafa0c687a33f030cc8fc88d3ecd1631716819ebce65c546", + "measured_on": "2026-10-07" } }, "remeasured": [ diff --git a/internal/guard/testdata/screens/catalogue.png b/internal/guard/testdata/screens/catalogue.png index 0a327cd0..132061d7 100644 Binary files a/internal/guard/testdata/screens/catalogue.png and b/internal/guard/testdata/screens/catalogue.png differ diff --git a/internal/guard/testdata/screens/catalogue.xml b/internal/guard/testdata/screens/catalogue.xml index 83a00b81..5834934e 100644 --- a/internal/guard/testdata/screens/catalogue.xml +++ b/internal/guard/testdata/screens/catalogue.xml @@ -1338,7 +1338,7 @@ - + Archives · 2 @@ -1385,154 +1385,164 @@ - Pictures · 10 + Mail · 1 + + + eml + Email message + + + + Pictures · 10 + + avif AV1 Image File Format - + bmp Windows Bitmap - + gif Graphics Interchange Format - + ico Windows Icon - + jpg JPEG - + jxl JPEG XL - + png Portable Network Graphics - + svg Scalable Vector Graphics - + tiff Tagged Image File Format - + webp WebP - + Sound · 1 - + wav Waveform Audio - + Text and data · 10 - + csv Comma-Separated Values - + geojson GeoJSON - + html HyperText Markup Language - + json JavaScript Object Notation - + log Server and application log - + md Markdown - + toml TOML - + txt Plain text - + xml Extensible Markup Language - + yaml YAML - + Video · 2 - + mp4 MP4 video - + webm @@ -1543,8 +1553,8 @@ - - + + diff --git a/internal/guard/testdata/screens/generate-menu-hovered.png b/internal/guard/testdata/screens/generate-menu-hovered.png index dd90b007..0c4d5caa 100644 Binary files a/internal/guard/testdata/screens/generate-menu-hovered.png and b/internal/guard/testdata/screens/generate-menu-hovered.png differ diff --git a/internal/guard/testdata/screens/generate-menu-hovered.xml b/internal/guard/testdata/screens/generate-menu-hovered.xml index 6e025a4e..6e7975f1 100644 --- a/internal/guard/testdata/screens/generate-menu-hovered.xml +++ b/internal/guard/testdata/screens/generate-menu-hovered.xml @@ -506,7 +506,7 @@ - + Archives · 2 @@ -553,154 +553,164 @@ - Pictures · 10 + Mail · 1 + + + eml + Email message + + + + Pictures · 10 + + avif AV1 Image File Format - + bmp Windows Bitmap - + gif Graphics Interchange Format - + ico Windows Icon - + jpg JPEG - + jxl JPEG XL - + png Portable Network Graphics - + svg Scalable Vector Graphics - + tiff Tagged Image File Format - + webp WebP - + Sound · 1 - + wav Waveform Audio - + Text and data · 10 - + csv Comma-Separated Values - + geojson GeoJSON - + html HyperText Markup Language - + json JavaScript Object Notation - + log Server and application log - + md Markdown - + toml TOML - + txt Plain text - + xml Extensible Markup Language - + yaml YAML - + Video · 2 - + mp4 MP4 video - + webm @@ -711,8 +721,8 @@ - - + + diff --git a/internal/guard/testdata/screens/generate-menu-keyed.png b/internal/guard/testdata/screens/generate-menu-keyed.png index ff4e980b..15d6dc79 100644 Binary files a/internal/guard/testdata/screens/generate-menu-keyed.png and b/internal/guard/testdata/screens/generate-menu-keyed.png differ diff --git a/internal/guard/testdata/screens/generate-menu-keyed.xml b/internal/guard/testdata/screens/generate-menu-keyed.xml index 1972aa90..f7b8c69b 100644 --- a/internal/guard/testdata/screens/generate-menu-keyed.xml +++ b/internal/guard/testdata/screens/generate-menu-keyed.xml @@ -506,7 +506,7 @@ - + Archives · 2 @@ -553,154 +553,164 @@ - Pictures · 10 + Mail · 1 + + + eml + Email message + + + + Pictures · 10 + + avif AV1 Image File Format - + bmp Windows Bitmap - + gif Graphics Interchange Format - + ico Windows Icon - + jpg JPEG - + jxl JPEG XL - + png Portable Network Graphics - + svg Scalable Vector Graphics - + tiff Tagged Image File Format - + webp WebP - + Sound · 1 - + wav Waveform Audio - + Text and data · 10 - + csv Comma-Separated Values - + geojson GeoJSON - + html HyperText Markup Language - + json JavaScript Object Notation - + log Server and application log - + md Markdown - + toml TOML - + txt Plain text - + xml Extensible Markup Language - + yaml YAML - + Video · 2 - + mp4 MP4 video - + webm @@ -711,8 +721,8 @@ - - + + diff --git a/internal/guard/testdata/screens/generate-menu.png b/internal/guard/testdata/screens/generate-menu.png index 596122d8..3657dc4d 100644 Binary files a/internal/guard/testdata/screens/generate-menu.png and b/internal/guard/testdata/screens/generate-menu.png differ diff --git a/internal/guard/testdata/screens/generate-menu.xml b/internal/guard/testdata/screens/generate-menu.xml index b19ad7b8..7368046b 100644 --- a/internal/guard/testdata/screens/generate-menu.xml +++ b/internal/guard/testdata/screens/generate-menu.xml @@ -506,7 +506,7 @@ - + Archives · 2 @@ -553,154 +553,164 @@ - Pictures · 10 + Mail · 1 + + + eml + Email message + + + + Pictures · 10 + + avif AV1 Image File Format - + bmp Windows Bitmap - + gif Graphics Interchange Format - + ico Windows Icon - + jpg JPEG - + jxl JPEG XL - + png Portable Network Graphics - + svg Scalable Vector Graphics - + tiff Tagged Image File Format - + webp WebP - + Sound · 1 - + wav Waveform Audio - + Text and data · 10 - + csv Comma-Separated Values - + geojson GeoJSON - + html HyperText Markup Language - + json JavaScript Object Notation - + log Server and application log - + md Markdown - + toml TOML - + txt Plain text - + xml Extensible Markup Language - + yaml YAML - + Video · 2 - + mp4 MP4 video - + webm @@ -711,8 +721,8 @@ - - + + diff --git a/internal/guard/testdata/screens/preset-menu-setting.png b/internal/guard/testdata/screens/preset-menu-setting.png index b7fe4523..83aa78c9 100644 Binary files a/internal/guard/testdata/screens/preset-menu-setting.png and b/internal/guard/testdata/screens/preset-menu-setting.png differ diff --git a/internal/guard/testdata/screens/preset-menu-setting.xml b/internal/guard/testdata/screens/preset-menu-setting.xml index 8309fc0b..88f2e799 100644 --- a/internal/guard/testdata/screens/preset-menu-setting.xml +++ b/internal/guard/testdata/screens/preset-menu-setting.xml @@ -469,7 +469,7 @@ - + Archives · 2 @@ -517,153 +517,163 @@ - Pictures · 10 + Mail · 1 + + + eml + Email message + + + + Pictures · 10 + + avif AV1 Image File Format - + bmp Windows Bitmap - + gif Graphics Interchange Format - + ico Windows Icon - + jpg JPEG - + jxl JPEG XL - + png Portable Network Graphics - + svg Scalable Vector Graphics - + tiff Tagged Image File Format - + webp WebP - + Sound · 1 - + wav Waveform Audio - + Text and data · 10 - + csv Comma-Separated Values - + geojson GeoJSON - + html HyperText Markup Language - + json JavaScript Object Notation - + log Server and application log - + md Markdown - + toml TOML - + txt Plain text - + xml Extensible Markup Language - + yaml YAML - + Video · 2 - + mp4 MP4 video - + webm @@ -674,8 +684,8 @@ - - + + diff --git a/internal/guard/testdata/screens/preset-menu.png b/internal/guard/testdata/screens/preset-menu.png index 857b6991..96e95fed 100644 Binary files a/internal/guard/testdata/screens/preset-menu.png and b/internal/guard/testdata/screens/preset-menu.png differ diff --git a/internal/guard/testdata/screens/preset-menu.xml b/internal/guard/testdata/screens/preset-menu.xml index 0f56f950..c0643ad0 100644 --- a/internal/guard/testdata/screens/preset-menu.xml +++ b/internal/guard/testdata/screens/preset-menu.xml @@ -370,7 +370,7 @@ - 31 files · 39.2 KB (40 138 B) · 29 formats · will go to /tfg/out + 32 files · 39.5 KB (40 427 B) · 30 formats · will go to /tfg/out diff --git a/internal/guard/testdata/screens/preset.png b/internal/guard/testdata/screens/preset.png index e8650466..7cbb12d2 100644 Binary files a/internal/guard/testdata/screens/preset.png and b/internal/guard/testdata/screens/preset.png differ diff --git a/internal/guard/testdata/screens/preset.xml b/internal/guard/testdata/screens/preset.xml index 58a3cd48..0d6ed30c 100644 --- a/internal/guard/testdata/screens/preset.xml +++ b/internal/guard/testdata/screens/preset.xml @@ -370,7 +370,7 @@ - 31 files · 39.2 KB (40 138 B) · 29 formats · will go to /tfg/out + 32 files · 39.5 KB (40 427 B) · 30 formats · will go to /tfg/out diff --git a/internal/guard/testdata/screens/recipe-on-a-preset.png b/internal/guard/testdata/screens/recipe-on-a-preset.png index 52ec110d..d209116c 100644 Binary files a/internal/guard/testdata/screens/recipe-on-a-preset.png and b/internal/guard/testdata/screens/recipe-on-a-preset.png differ diff --git a/internal/guard/testdata/screens/recipe-on-a-preset.xml b/internal/guard/testdata/screens/recipe-on-a-preset.xml index a3020fdf..bdbf6888 100644 --- a/internal/guard/testdata/screens/recipe-on-a-preset.xml +++ b/internal/guard/testdata/screens/recipe-on-a-preset.xml @@ -658,7 +658,7 @@ - 32 files · 10.0 MB (10 525 898 B) · 29 formats · will go to /tfg/out + 33 files · 10.0 MB (10 526 187 B) · 30 formats · will go to /tfg/out diff --git a/internal/guard/textformats_test.go b/internal/guard/textformats_test.go index 521b912b..a69d4d2d 100644 --- a/internal/guard/textformats_test.go +++ b/internal/guard/textformats_test.go @@ -444,7 +444,7 @@ func TestAnSVGDrawingCarriesRealShapes(t *testing.T) { // This is the shape docs/OBSERVATIONS.md now calls out: an audit of // completeness has to run FROM THE SOURCE towards the list. Walking the entries // already written down cannot, by construction, find what is missing from them. -var textFormats = []string{"txt", "md", "log", "csv", "json", "xml", "html", "svg", "yaml", "toml", "geojson"} +var textFormats = []string{"txt", "md", "log", "csv", "json", "xml", "html", "svg", "yaml", "toml", "geojson", "eml"} var binaryFormats = []string{"avif", "bmp", "docx", "gif", "ico", "jpg", "jxl", "mp4", "pdf", "png", "pptx", "targz", "tiff", "wav", "webm", "webp", "xlsx", "zip"} diff --git a/internal/gui/parts/filekind.go b/internal/gui/parts/filekind.go index 115d9719..d7174251 100644 --- a/internal/gui/parts/filekind.go +++ b/internal/gui/parts/filekind.go @@ -61,6 +61,8 @@ func KindOfFile(id string) fyne.Resource { return theme.MediaMusicIcon() case kindMoving: return theme.MediaVideoIcon() + case kindMail: + return theme.MailComposeIcon() case kindWords: return theme.ListIcon() case kindUnknown: @@ -90,6 +92,8 @@ func KindHeading(id string) string { return text.ListKindSound() case kindMoving: return text.ListKindVideo() + case kindMail: + return text.ListKindMail() case kindWords: return text.ListKindText() case kindUnknown: @@ -125,6 +129,9 @@ const ( // a film arrives is the day somebody is thinking about something else. kindSound kindMoving + // kindMail is a message a mail program opens. eml was the first, on + // 2026-10-07, and the backlog names four more of the family. + kindMail // kindWords is text and structured data: what a parser reads. kindWords ) @@ -154,6 +161,7 @@ var fileKinds = map[string]fileKind{ "wav": kindSound, "webm": kindMoving, "mp4": kindMoving, + "eml": kindMail, "csv": kindWords, "geojson": kindWords, "html": kindWords, diff --git a/internal/gui/text/locale/en.json b/internal/gui/text/locale/en.json index 1c143845..c721910d 100644 --- a/internal/gui/text/locale/en.json +++ b/internal/gui/text/locale/en.json @@ -561,6 +561,11 @@ "hash": "sha256-b4e929d8bcfe", "other": "Documents" }, + "ListKindMail": { + "description": "A heading inside an open list of formats, over the formats of one kind of file.", + "hash": "sha256-07ed6ccf6bf6", + "other": "Mail" + }, "ListKindPictures": { "description": "A heading inside an open list of formats, over the formats of one kind of file.", "hash": "sha256-1e6245009d67", diff --git a/internal/gui/text/locale/pl.json b/internal/gui/text/locale/pl.json index f969d35c..20cac93e 100644 --- a/internal/gui/text/locale/pl.json +++ b/internal/gui/text/locale/pl.json @@ -111,6 +111,7 @@ "ListJoinedOr": { "hash": "sha256-c9be11158971", "other": "{{.Head}} albo {{.Last}}" }, "ListKindArchives": { "hash": "sha256-e404aa80d8df", "other": "Archiwa" }, "ListKindDocuments": { "hash": "sha256-b4e929d8bcfe", "other": "Dokumenty" }, + "ListKindMail": { "hash": "sha256-07ed6ccf6bf6", "other": "Poczta" }, "ListKindPictures": { "hash": "sha256-1e6245009d67", "other": "Obrazy" }, "ListKindSound": { "hash": "sha256-46124e023603", "other": "Dźwięk" }, "ListKindText": { "hash": "sha256-c04c0f52d26a", "other": "Tekst i dane" }, diff --git a/internal/gui/text/locale/registry/en.json b/internal/gui/text/locale/registry/en.json index 89689916..7a8ec5b2 100644 --- a/internal/gui/text/locale/registry/en.json +++ b/internal/gui/text/locale/registry/en.json @@ -164,6 +164,21 @@ "hash": "sha256-2c624232cdd2", "other": "8" }, + "Choice.body.alternative": { + "description": "One entry of the body list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-27747689baf9", + "other": "alternative" + }, + "Choice.body.html": { + "description": "One entry of the body list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-e5e239560bc1", + "other": "html" + }, + "Choice.body.plain": { + "description": "One entry of the body list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-a116c9ed46d6", + "other": "plain" + }, "Choice.compression.best": { "description": "One entry of the compression list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", "hash": "sha256-8f4375ec65c2", @@ -479,6 +494,26 @@ "hash": "sha256-b4dc66dde806", "other": "off" }, + "Choice.filename_style.both": { + "description": "One entry of the filename_style list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-ff7772053abf", + "other": "both" + }, + "Choice.filename_style.content-type": { + "description": "One entry of the filename_style list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-846b3a9229ce", + "other": "content-type" + }, + "Choice.filename_style.rfc2047": { + "description": "One entry of the filename_style list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-1eeb8be955da", + "other": "rfc2047" + }, + "Choice.filename_style.rfc2231": { + "description": "One entry of the filename_style list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c4a81dcefb6e", + "other": "rfc2231" + }, "Choice.format.avif": { "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", "hash": "sha256-2f47c57aae31", @@ -499,6 +534,11 @@ "hash": "sha256-584cb925e6ad", "other": "docx" }, + "Choice.format.eml": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-13033c0419d0", + "other": "eml" + }, "Choice.format.geojson": { "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", "hash": "sha256-64bce794becb", @@ -734,6 +774,21 @@ "hash": "sha256-2b3b18120924", "other": "polygon" }, + "Choice.headers.ascii": { + "description": "One entry of the headers list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c543ece81605", + "other": "ascii" + }, + "Choice.headers.encoded": { + "description": "One entry of the headers list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-766adc67b02b", + "other": "encoded" + }, + "Choice.headers.utf8": { + "description": "One entry of the headers list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-941b7ecd47e5", + "other": "utf8" + }, "Choice.ids.mixed": { "description": "One entry of the ids list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", "hash": "sha256-3f8fee624f43", @@ -799,6 +854,16 @@ "hash": "sha256-39e741eddb03", "other": "lf" }, + "Choice.line_endings.crlf": { + "description": "One entry of the line_endings list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c613dc516f32", + "other": "crlf" + }, + "Choice.line_endings.lf": { + "description": "One entry of the line_endings list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-39e741eddb03", + "other": "lf" + }, "Choice.methods.get": { "description": "One entry of the methods list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", "hash": "sha256-2998b3232d29", @@ -934,6 +999,21 @@ "hash": "sha256-7587669ca42e", "other": "fragment" }, + "Choice.text_encoding.7bit": { + "description": "One entry of the text_encoding list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-af6ec1d82852", + "other": "7bit" + }, + "Choice.text_encoding.8bit": { + "description": "One entry of the text_encoding list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-e318653d2946", + "other": "8bit" + }, + "Choice.text_encoding.quoted-printable": { + "description": "One entry of the text_encoding list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-7956a9e2a40b", + "other": "quoted-printable" + }, "Choice.timestamps.advancing": { "description": "One entry of the timestamps list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", "hash": "sha256-3656fa4d5539", @@ -1029,6 +1109,46 @@ "hash": "sha256-5d0141e9b1da", "other": "How many paragraphs of text the document holds. The rest of the size you asked for is carried by a part of the package that is not shown." }, + "Detail.format/eml.attachment_format": { + "description": "The sentence under the attachment_format setting of the eml format, after what it takes.", + "hash": "sha256-991e7874cdb1", + "other": "The format of the attached files. Run tfg formats to see what this build supports." + }, + "Detail.format/eml.attachment_size": { + "description": "The sentence under the attachment_size setting of the eml format, after what it takes.", + "hash": "sha256-c6ca8985e21d", + "other": "How big each attached file is. The message grows by about a third more, because attached files travel in base64." + }, + "Detail.format/eml.attachments": { + "description": "The sentence under the attachments setting of the eml format, after what it takes.", + "hash": "sha256-43c96b5ef075", + "other": "How many files the message carries. Use contains instead when the files are not all alike. mailparser refuses a message of more than 1000 MIME entities, and the manifest gives this one's count as mime_entities." + }, + "Detail.format/eml.body": { + "description": "The sentence under the body setting of the eml format, after what it takes.", + "hash": "sha256-5dc479375b75", + "other": "Which text the message carries. plain is one text part and html one HTML part. alternative carries both with the same words, and a mail program shows one of them." + }, + "Detail.format/eml.filename_style": { + "description": "The sentence under the filename_style setting of the eml format, after what it takes.", + "hash": "sha256-96e791aa7eac", + "other": "How the names of attached files are written. rfc2231 is the standard way. rfc2047 is the encoded form RFC 2047 forbids inside a name, and many readers decode it anyway. both writes the two, and content-type puts the name only where old mail programs looked for it. Go's standard library reads no name from content-type and the encoded text itself from rfc2047. Unless headers is encoded a name is written as it is and only where it goes changes, so rfc2047 and both write the same bytes." + }, + "Detail.format/eml.headers": { + "description": "The sentence under the headers setting of the eml format, after what it takes.", + "hash": "sha256-5002e2b42e63", + "other": "Whether the subject, the names of the sender and the recipient and the names of attached files carry letters outside ASCII, and how. encoded writes them the way RFC 2047 and RFC 2231 say. utf8 writes them as they are (RFC 6532), and Python's email package reads that and reports a defect in the From and To lines." + }, + "Detail.format/eml.line_endings": { + "description": "The sentence under the line_endings setting of the eml format, after what it takes.", + "hash": "sha256-42b29dfbf2d1", + "other": "How every line of the file ends. The standard is crlf. Python, MimeKit, mailparser and Go's standard library all read lf too, so the difference shows in programs that split the lines themselves." + }, + "Detail.format/eml.text_encoding": { + "description": "The sentence under the text_encoding setting of the eml format, after what it takes.", + "hash": "sha256-b8ce76fdb974", + "other": "How the text is written. 7bit keeps it to plain ASCII. quoted-printable and 8bit add a line in Polish and Japanese, because that is what they are for - quoted-printable spells those letters as =C5=BC, and 8bit writes them as they are." + }, "Detail.format/geojson.altitude": { "description": "The sentence under the altitude setting of the geojson format, after what it takes.", "hash": "sha256-702be3958a4b", @@ -1614,6 +1734,11 @@ "hash": "sha256-a1fb88e6372c", "other": "Word (Office Open XML)" }, + "Format.eml": { + "description": "What the eml format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-eb653a6eee22", + "other": "Email message" + }, "Format.geojson": { "description": "What the geojson format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", "hash": "sha256-84fe8f7f1c87", @@ -1739,6 +1864,11 @@ "hash": "sha256-eaca4b306928", "other": "ZIP" }, + "Group.format/eml.Attached files": { + "description": "The heading over a block of settings of the eml format.", + "hash": "sha256-ecc5a28d1f5e", + "other": "Attached files" + }, "Group.format/pdf.Document properties": { "description": "The heading over a block of settings of the pdf format.", "hash": "sha256-7115a778ab47", @@ -1829,6 +1959,21 @@ "hash": "sha256-cd0da1397b61", "other": "Antimeridian" }, + "Label.attachment_format": { + "description": "The name beside the box of a setting a recipe writes as attachment_format.", + "hash": "sha256-29f25b6c026f", + "other": "Attachment format" + }, + "Label.attachment_size": { + "description": "The name beside the box of a setting a recipe writes as attachment_size.", + "hash": "sha256-151d41e8a53c", + "other": "Attachment size" + }, + "Label.attachments": { + "description": "The name beside the box of a setting a recipe writes as attachments.", + "hash": "sha256-634de1147750", + "other": "Attachments" + }, "Label.author": { "description": "The name beside the box of a setting a recipe writes as author.", "hash": "sha256-d95082a2ee57", @@ -1844,6 +1989,11 @@ "hash": "sha256-cc62f2e89a82", "other": "Bit depth" }, + "Label.body": { + "description": "The name beside the box of a setting a recipe writes as body.", + "hash": "sha256-6ccaa6415b5e", + "other": "Body" + }, "Label.bom": { "description": "The name beside the box of a setting a recipe writes as bom.", "hash": "sha256-e39338101951", @@ -1979,6 +2129,11 @@ "hash": "sha256-50009ce1da4d", "other": "File" }, + "Label.filename_style": { + "description": "The name beside the box of a setting a recipe writes as filename_style.", + "hash": "sha256-fbfbf4d41f13", + "other": "Filename style" + }, "Label.folder": { "description": "The name beside the box of what the checksum-write tool works on.", "hash": "sha256-74ccd4330384", @@ -2019,6 +2174,11 @@ "hash": "sha256-ba5caa4285a8", "other": "Header" }, + "Label.headers": { + "description": "The name beside the box of a setting a recipe writes as headers.", + "hash": "sha256-194e9fe656a1", + "other": "Headers" + }, "Label.height": { "description": "The name beside the box of a setting a recipe writes as height.", "hash": "sha256-bfe2eca40179", @@ -2064,6 +2224,11 @@ "hash": "sha256-c4f7d0b83542", "other": "Line ending" }, + "Label.line_endings": { + "description": "The name beside the box of a setting a recipe writes as line_endings.", + "hash": "sha256-c948fba2b0dd", + "other": "Line endings" + }, "Label.methods": { "description": "The name beside the box of a setting a recipe writes as methods.", "hash": "sha256-8696622f3441", @@ -2174,6 +2339,11 @@ "hash": "sha256-68971283841a", "other": "Subject" }, + "Label.text_encoding": { + "description": "The name beside the box of a setting a recipe writes as text_encoding.", + "hash": "sha256-f6726ca0b9fe", + "other": "Text encoding" + }, "Label.timestamps": { "description": "The name beside the box of a setting a recipe writes as timestamps.", "hash": "sha256-755f099dbf49", diff --git a/internal/gui/text/locale/registry/pl.json b/internal/gui/text/locale/registry/pl.json index b11523a7..1cf30bb2 100644 --- a/internal/gui/text/locale/registry/pl.json +++ b/internal/gui/text/locale/registry/pl.json @@ -32,6 +32,9 @@ "Choice.bit_depth.24": { "hash": "sha256-c2356069e9d1", "other": "24" }, "Choice.bit_depth.32": { "hash": "sha256-e29c9c180c62", "other": "32" }, "Choice.bit_depth.8": { "hash": "sha256-2c624232cdd2", "other": "8" }, + "Choice.body.alternative": { "hash": "sha256-27747689baf9", "other": "tekst i html" }, + "Choice.body.html": { "hash": "sha256-e5e239560bc1", "other": "html" }, + "Choice.body.plain": { "hash": "sha256-a116c9ed46d6", "other": "zwykły tekst" }, "Choice.compression.best": { "hash": "sha256-8f4375ec65c2", "other": "najlepsza" }, "Choice.compression.default": { "hash": "sha256-37a8eec1ce19", "other": "domyślna" }, "Choice.compression.fast": { "hash": "sha256-115dc3606fbf", "other": "szybka" }, @@ -95,10 +98,15 @@ "Choice.far-over.10x": { "hash": "sha256-687f5e475cbd", "other": "10x" }, "Choice.far-over.2x": { "hash": "sha256-b595b214b5d4", "other": "2x" }, "Choice.far-over.off": { "hash": "sha256-b4dc66dde806", "other": "wyłączone" }, + "Choice.filename_style.both": { "hash": "sha256-ff7772053abf", "other": "oba" }, + "Choice.filename_style.content-type": { "hash": "sha256-846b3a9229ce", "other": "content-type" }, + "Choice.filename_style.rfc2047": { "hash": "sha256-1eeb8be955da", "other": "rfc2047" }, + "Choice.filename_style.rfc2231": { "hash": "sha256-c4a81dcefb6e", "other": "rfc2231" }, "Choice.format.avif": { "hash": "sha256-2f47c57aae31", "other": "avif" }, "Choice.format.bmp": { "hash": "sha256-7685a5cb2b57", "other": "bmp" }, "Choice.format.csv": { "hash": "sha256-53bb9b0b38ac", "other": "csv" }, "Choice.format.docx": { "hash": "sha256-584cb925e6ad", "other": "docx" }, + "Choice.format.eml": { "hash": "sha256-13033c0419d0", "other": "eml" }, "Choice.format.geojson": { "hash": "sha256-64bce794becb", "other": "geojson" }, "Choice.format.gif": { "hash": "sha256-3b34c6906f7e", "other": "gif" }, "Choice.format.html": { "hash": "sha256-e5e239560bc1", "other": "html" }, @@ -146,6 +154,9 @@ "Choice.geometry.multipolygon": { "hash": "sha256-d9cfa92a41b7", "other": "multipoligon" }, "Choice.geometry.point": { "hash": "sha256-251fecd592e5", "other": "punkt" }, "Choice.geometry.polygon": { "hash": "sha256-2b3b18120924", "other": "poligon" }, + "Choice.headers.ascii": { "hash": "sha256-c543ece81605", "other": "ascii" }, + "Choice.headers.encoded": { "hash": "sha256-766adc67b02b", "other": "kodowane" }, + "Choice.headers.utf8": { "hash": "sha256-941b7ecd47e5", "other": "utf8" }, "Choice.ids.mixed": { "hash": "sha256-3f8fee624f43", "other": "mieszane" }, "Choice.ids.none": { "hash": "sha256-140bedbf9c3f", "other": "brak" }, "Choice.ids.number": { "hash": "sha256-12886f9d0005", "other": "liczba" }, @@ -159,6 +170,8 @@ "Choice.level_mix.realistic": { "hash": "sha256-9d2f45fd706a", "other": "realistycznie" }, "Choice.line_ending.crlf": { "hash": "sha256-c613dc516f32", "other": "crlf" }, "Choice.line_ending.lf": { "hash": "sha256-39e741eddb03", "other": "lf" }, + "Choice.line_endings.crlf": { "hash": "sha256-c613dc516f32", "other": "crlf" }, + "Choice.line_endings.lf": { "hash": "sha256-39e741eddb03", "other": "lf" }, "Choice.methods.get": { "hash": "sha256-2998b3232d29", "other": "get" }, "Choice.methods.mixed": { "hash": "sha256-3f8fee624f43", "other": "mieszane" }, "Choice.methods.read": { "hash": "sha256-3316348dbadf", "other": "odczyt" }, @@ -186,6 +199,9 @@ "Choice.status_mix.success": { "hash": "sha256-aee408847d35", "other": "sukcesy" }, "Choice.structure.document": { "hash": "sha256-43cc23fa52b8", "other": "dokument" }, "Choice.structure.fragment": { "hash": "sha256-7587669ca42e", "other": "fragment" }, + "Choice.text_encoding.7bit": { "hash": "sha256-af6ec1d82852", "other": "7bit" }, + "Choice.text_encoding.8bit": { "hash": "sha256-e318653d2946", "other": "8bit" }, + "Choice.text_encoding.quoted-printable": { "hash": "sha256-7956a9e2a40b", "other": "quoted-printable" }, "Choice.timestamps.advancing": { "hash": "sha256-3656fa4d5539", "other": "rosnące" }, "Choice.timestamps.fixed": { "hash": "sha256-992a93455c71", "other": "stałe" }, "Choice.unlocated.all": { "hash": "sha256-5ef5ef0364b6", "other": "wszystkie" }, @@ -205,6 +221,14 @@ "Detail.format/csv.line_ending": { "hash": "sha256-d802b082ce8b", "other": "Jak kończy się każdy wiersz. Wybierz crlf, żeby dostać kształt, którego wymaga RFC 4180 i który zapisuje Excel." }, "Detail.format/csv.quote_style": { "hash": "sha256-a2427ed0240b", "other": "Które pola mają cudzysłowy. Gdy wybierzesz brak, opis przestaje też zawierać separatory, bo pole bez cudzysłowu nie może go pomieścić." }, "Detail.format/docx.paragraphs": { "hash": "sha256-5d0141e9b1da", "other": "Ile akapitów tekstu zawiera dokument. Resztę zamówionego rozmiaru niesie niewidoczna część paczki." }, + "Detail.format/eml.attachment_format": { "hash": "sha256-991e7874cdb1", "other": "Format załączonych plików. Uruchom tfg formats, żeby zobaczyć, co obsługuje ta wersja." }, + "Detail.format/eml.attachment_size": { "hash": "sha256-c6ca8985e21d", "other": "Jak duży jest każdy załączony plik. Wiadomość rośnie o mniej więcej jedną trzecią więcej, bo załączone pliki są przesyłane w base64." }, + "Detail.format/eml.attachments": { "hash": "sha256-43c96b5ef075", "other": "Ile plików niesie wiadomość. Gdy pliki nie są jednakowe, użyj zamiast tego contains. mailparser odmawia wiadomości z więcej niż 1000 encji MIME, a manifest podaje ich liczbę dla tej wiadomości jako mime_entities." }, + "Detail.format/eml.body": { "hash": "sha256-5dc479375b75", "other": "Jaki tekst niesie wiadomość. zwykły tekst to jedna część tekstowa, a html jedna część HTML. tekst i html niesie obie z tymi samymi słowami, a program pocztowy pokazuje jedną z nich." }, + "Detail.format/eml.filename_style": { "hash": "sha256-96e791aa7eac", "other": "Jak zapisane są nazwy załączonych plików. rfc2231 to sposób standardowy. rfc2047 to forma kodowana, której RFC 2047 zabrania wewnątrz nazwy, a wiele czytników i tak ją dekoduje. oba zapisuje obie, a content-type umieszcza nazwę tylko tam, gdzie szukały jej stare programy pocztowe. Biblioteka standardowa Go nie czyta nazwy z content-type, a z rfc2047 oddaje sam zakodowany tekst. Gdy headers nie jest ustawione na kodowane, nazwa jest zapisana tak, jak jest, i zmienia się tylko jej miejsce, więc rfc2047 i oba zapisują te same bajty." }, + "Detail.format/eml.headers": { "hash": "sha256-5002e2b42e63", "other": "Czy temat, nazwy nadawcy i odbiorcy oraz nazwy załączonych plików mają litery spoza ASCII, i jak są zapisane. kodowane zapisuje je tak, jak każą RFC 2047 i RFC 2231. utf8 zapisuje je tak, jak są (RFC 6532), a pakiet email Pythona czyta to i zgłasza defekt w liniach From i To." }, + "Detail.format/eml.line_endings": { "hash": "sha256-42b29dfbf2d1", "other": "Jak kończy się każda linia pliku. Standardem jest crlf. Python, MimeKit, mailparser i biblioteka standardowa Go czytają też lf, więc różnicę widać w programach, które same dzielą linie." }, + "Detail.format/eml.text_encoding": { "hash": "sha256-b8ce76fdb974", "other": "Jak zapisany jest tekst. 7bit trzyma go w czystym ASCII. quoted-printable i 8bit dodają linię po polsku i po japońsku, bo do tego służą - quoted-printable zapisuje te litery jako =C5=BC, a 8bit tak, jak są." }, "Detail.format/geojson.altitude": { "hash": "sha256-702be3958a4b", "other": "Czy każda pozycja niesie trzecią liczbę, wysokość w metrach od -400 do 8848. Program, który czyta dwie liczby na pozycję, musi ją pominąć albo odrzucić plik." }, "Detail.format/geojson.antimeridian": { "hash": "sha256-e7755a355839", "other": "Czy każda linia i każdy obrys przecina 180. południk bez rozcięcia na dwie części. RFC 7946 każe takie kształty rozcinać, a czytniki się różnią - czytany jako płaskie współrzędne, przecinający obrys przecina sam siebie i biegnie w drugą stronę. Pojedynczy punkt nie może przeciąć i leży obok." }, "Detail.format/geojson.bbox": { "hash": "sha256-2046a38a38a9", "other": "Czy kolekcja i każdy obiekt z położeniem niosą bbox, prostokąt, w którym leżą ich współrzędne. Bbox kolekcji stoi za jej obiektami, bo jej zasięg jest znany dopiero po ostatnim, a prostokąt przecinający antypołudnik ma zachodnią krawędź większą niż wschodnią, jak każe RFC 7946." }, @@ -322,6 +346,7 @@ "Format.bmp": { "hash": "sha256-a9f24d64c3d9", "other": "Windows Bitmap" }, "Format.csv": { "hash": "sha256-6c61f1d4908c", "other": "Comma-Separated Values" }, "Format.docx": { "hash": "sha256-a1fb88e6372c", "other": "Word (Office Open XML)" }, + "Format.eml": { "hash": "sha256-eb653a6eee22", "other": "Wiadomość e-mail" }, "Format.geojson": { "hash": "sha256-84fe8f7f1c87", "other": "GeoJSON" }, "Format.gif": { "hash": "sha256-9a8783e957eb", "other": "Graphics Interchange Format" }, "Format.html": { "hash": "sha256-0bd4039bc33f", "other": "HyperText Markup Language" }, @@ -347,6 +372,7 @@ "Format.xml": { "hash": "sha256-f4210757fbab", "other": "Extensible Markup Language" }, "Format.yaml": { "hash": "sha256-9b6e8abe425e", "other": "YAML" }, "Format.zip": { "hash": "sha256-eaca4b306928", "other": "ZIP" }, + "Group.format/eml.Attached files": { "hash": "sha256-ecc5a28d1f5e", "other": "Załączone pliki" }, "Group.format/pdf.Document properties": { "hash": "sha256-7115a778ab47", "other": "Właściwości dokumentu" }, "Input.checksum-check.checksum_file": { "hash": "sha256-90d7baf0d228", "other": "Plik sum kontrolnych. Ścieżki w nim są czytane z folderu, w którym leży, i nic nie jest zapisywane." }, "Input.checksum-write.folder": { "hash": "sha256-3040f185111b", "other": "Folder do spisania. Każdy plik w nim jest czytany, a plik sum kontrolnych jest zapisywany do niego." }, @@ -365,9 +391,13 @@ "Label.allow": { "hash": "sha256-e213c161d5ce", "other": "Dozwolone" }, "Label.altitude": { "hash": "sha256-cbc2e75740ca", "other": "Wysokość" }, "Label.antimeridian": { "hash": "sha256-cd0da1397b61", "other": "Antypołudnik" }, + "Label.attachment_format": { "hash": "sha256-29f25b6c026f", "other": "Format załączników" }, + "Label.attachment_size": { "hash": "sha256-151d41e8a53c", "other": "Rozmiar załączników" }, + "Label.attachments": { "hash": "sha256-634de1147750", "other": "Liczba załączników" }, "Label.author": { "hash": "sha256-d95082a2ee57", "other": "Autor" }, "Label.bbox": { "hash": "sha256-708f44b1bd98", "other": "Ramka (bbox)" }, "Label.bit_depth": { "hash": "sha256-cc62f2e89a82", "other": "Głębia bitowa" }, + "Label.body": { "hash": "sha256-6ccaa6415b5e", "other": "Treść" }, "Label.bom": { "hash": "sha256-e39338101951", "other": "BOM" }, "Label.bulk": { "hash": "sha256-3140100df131", "other": "Przesyłanie masowe" }, "Label.bytes": { "hash": "sha256-4d8000301fcc", "other": "Bajty" }, @@ -395,6 +425,7 @@ "Label.expected": { "hash": "sha256-ca99b7f1b14e", "other": "Oczekiwana suma" }, "Label.far-over": { "hash": "sha256-874367c30d4e", "other": "Daleko ponad limit" }, "Label.file": { "hash": "sha256-50009ce1da4d", "other": "Plik" }, + "Label.filename_style": { "hash": "sha256-fbfbf4d41f13", "other": "Zapis nazw plików" }, "Label.folder": { "hash": "sha256-74ccd4330384", "other": "Folder" }, "Label.format": { "hash": "sha256-2f343666aaa8", "other": "Format" }, "Label.formats": { "hash": "sha256-9f01769a4278", "other": "Formaty" }, @@ -403,6 +434,7 @@ "Label.frames": { "hash": "sha256-2c447e8d0598", "other": "Klatki" }, "Label.geometry": { "hash": "sha256-fa9da5754ccf", "other": "Geometria" }, "Label.header": { "hash": "sha256-ba5caa4285a8", "other": "Nagłówek" }, + "Label.headers": { "hash": "sha256-194e9fe656a1", "other": "Nagłówki" }, "Label.height": { "hash": "sha256-bfe2eca40179", "other": "Wysokość" }, "Label.holes": { "hash": "sha256-158a3b71a54c", "other": "Dziury" }, "Label.ids": { "hash": "sha256-d9aa9d4aca97", "other": "Identyfikatory" }, @@ -412,6 +444,7 @@ "Label.level_mix": { "hash": "sha256-cc4f6ea7c7fc", "other": "Poziomy ważności" }, "Label.limit": { "hash": "sha256-674b0ed54bf7", "other": "Limit" }, "Label.line_ending": { "hash": "sha256-c4f7d0b83542", "other": "Koniec linii" }, + "Label.line_endings": { "hash": "sha256-c948fba2b0dd", "other": "Końce linii" }, "Label.methods": { "hash": "sha256-8696622f3441", "other": "Metody" }, "Label.modified": { "hash": "sha256-e8ce5dcaf408", "other": "Zmodyfikowano" }, "Label.orientation": { "hash": "sha256-e3d11242200f", "other": "Orientacja" }, @@ -434,6 +467,7 @@ "Label.status_mix": { "hash": "sha256-e5e3006b5ecd", "other": "Kody odpowiedzi" }, "Label.structure": { "hash": "sha256-7203c2b3dad3", "other": "Struktura" }, "Label.subject": { "hash": "sha256-68971283841a", "other": "Temat" }, + "Label.text_encoding": { "hash": "sha256-f6726ca0b9fe", "other": "Kodowanie tekstu" }, "Label.timestamps": { "hash": "sha256-755f099dbf49", "other": "Znaczniki czasu" }, "Label.title": { "hash": "sha256-7e8cd2056da7", "other": "Tytuł" }, "Label.unlocated": { "hash": "sha256-9d9b671e4837", "other": "Bez położenia" }, diff --git a/internal/gui/text/locale/said/en.json b/internal/gui/text/locale/said/en.json index 8ca764d4..9ee14031 100644 --- a/internal/gui/text/locale/said/en.json +++ b/internal/gui/text/locale/said/en.json @@ -521,6 +521,36 @@ "one": "{{.Count}} paragraph", "other": "{{.Count}} paragraphs" }, + "eml.AMessageHoldsItsHeaderLines": { + "description": "Said by internal/format/eml - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-5b3dd4df2ace", + "other": "a message holds its header lines, its parts and every attached file in base64, which is about a third larger than the file itself" + }, + "eml.AMessageHoldsItsHeaderLinesAndText": { + "description": "Said by internal/format/eml - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-1e727c17f578", + "other": "a message holds its header lines and the lines that open its text" + }, + "eml.AMessageThisLargeCannotBe": { + "description": "Said by internal/format/eml - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-ff6a6f0065c8", + "other": "a message this large cannot be counted to the byte in this build" + }, + "eml.AskForAttachmentsOrFewer": { + "description": "Said by internal/format/eml - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Max}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2145e05752c5", + "other": "Ask for {{.Max}} attachments or fewer." + }, + "eml.AskForSmallerAttachments": { + "description": "Said by internal/format/eml - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-871e78c75c85", + "other": "Ask for fewer or smaller attachments." + }, + "eml.TheAttachedFileCannotBe": { + "description": "Said by internal/format/eml - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Err}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-cb66d6df0c0f", + "other": "eml: the attached {{.Format}} file cannot be made: {{.Err}}" + }, "engine.CannotCreateOutDir": { "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Dir}}, {{.Cause}}, written the way the program writes them - keep each spelled exactly that way.", "hash": "sha256-324037f71709", @@ -1241,13 +1271,13 @@ }, "format.ContentsConflict": { "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Keys}}, written the way the program writes them - keep each spelled exactly that way.", - "hash": "sha256-c02ff58536fe", - "other": "{{.Format}}: contains and the {{.Keys}} properties both say what the archive holds. Keep contains and drop the properties, or the other way round" + "hash": "sha256-f8af78b330b1", + "other": "{{.Format}}: contains and the {{.Keys}} properties both say which files it holds. Keep contains and drop the properties, or the other way round" }, "format.ContentsConflictOne": { "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", - "hash": "sha256-274eb866ce96", - "other": "{{.Format}}: contains and the {{.Key}} property both say what the archive holds. Keep contains and drop the properties, or the other way round" + "hash": "sha256-44b774ac5486", + "other": "{{.Format}}: contains and the {{.Key}} property both say which files it holds. Keep contains and drop the property, or the other way round" }, "format.CountOf": { "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Number}}, {{.Unit}}, written the way the program writes them - keep each spelled exactly that way.", @@ -1387,8 +1417,8 @@ }, "format.NestingUnsupported": { "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Inner}}, written the way the program writes them - keep each spelled exactly that way.", - "hash": "sha256-f2cd29c6111d", - "other": "{{.Format}} cannot hold {{.Inner}} yet - an archive inside an archive needs a depth limit first. Hold a different format, or build the inner archive as its own target" + "hash": "sha256-f9a7add09c20", + "other": "{{.Format}} cannot hold {{.Inner}} yet - a file inside a file of its own format needs a depth limit first. Hold a different format, or make the inner file its own target" }, "format.NoCalendarHasThatDayTime": { "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", diff --git a/internal/gui/text/locale/said/pl.json b/internal/gui/text/locale/said/pl.json index fcd7eb95..69dd2770 100644 --- a/internal/gui/text/locale/said/pl.json +++ b/internal/gui/text/locale/said/pl.json @@ -103,6 +103,12 @@ "damage.UnknownWhat": { "hash": "sha256-cfd2875327a9", "other": "nie ma uszkodzenia o nazwie {{.Damage}}" }, "damage.UnknownWhy": { "hash": "sha256-f2720ea32a8a", "other": "uszkodzenie musi być znane tej wersji, a nieznane zostawiłoby plik nienaruszony, podczas gdy manifest nazywałby go uszkodzonym" }, "docx.Paragraphs": { "hash": "sha256-45316182b77a", "one": "{{.Count}} akapitem", "few": "{{.Count}} akapitami", "many": "{{.Count}} akapitami", "other": "{{.Count}} akapitu" }, + "eml.AMessageHoldsItsHeaderLines": { "hash": "sha256-5b3dd4df2ace", "other": "wiadomość zawiera swoje linie nagłówka, swoje części i każdy załączony plik w base64, który jest o mniej więcej jedną trzecią większy niż sam plik" }, + "eml.AMessageHoldsItsHeaderLinesAndText": { "hash": "sha256-1e727c17f578", "other": "wiadomość zawiera swoje linie nagłówka i linie, którymi zaczyna się jej tekst" }, + "eml.AMessageThisLargeCannotBe": { "hash": "sha256-ff6a6f0065c8", "other": "tak dużej wiadomości ta wersja nie potrafi policzyć co do bajta" }, + "eml.AskForAttachmentsOrFewer": { "hash": "sha256-2145e05752c5", "other": "Poproś o najwyżej {{.Max}} załączników." }, + "eml.AskForSmallerAttachments": { "hash": "sha256-871e78c75c85", "other": "Poproś o mniej albo mniejsze załączniki." }, + "eml.TheAttachedFileCannotBe": { "hash": "sha256-cb66d6df0c0f", "other": "eml: załączony plik {{.Format}} nie może powstać: {{.Err}}" }, "engine.CannotCreateOutDir": { "hash": "sha256-324037f71709", "other": "nie można utworzyć katalogu wyjściowego {{.Dir}}: {{.Cause}}" }, "engine.CannotStart": { "hash": "sha256-114c2e153b9d", "other": "nie można zacząć przebiegu w {{.Dir}}: {{.Cause}}" }, "engine.Character": { "hash": "sha256-add48a171334", "other": "znak {{.Character}}" }, @@ -246,8 +252,8 @@ "format.CannotTake": { "hash": "sha256-f4cf771e6ee1", "other": "{{.Format}} nie przyjmuje {{.Key}}" }, "format.CannotTakeWhy": { "hash": "sha256-657cfcd8b903", "other": "{{.What}} - {{.Why}}" }, "format.CannotTakeWhyFix": { "hash": "sha256-ba47f922ee57", "other": "{{.What}} - {{.Why}}. {{.Fix}}" }, - "format.ContentsConflict": { "hash": "sha256-c02ff58536fe", "other": "{{.Format}}: contains i właściwości {{.Keys}} mówią obie, co zawiera archiwum. Zostaw contains i usuń właściwości albo odwrotnie" }, - "format.ContentsConflictOne": { "hash": "sha256-274eb866ce96", "other": "{{.Format}}: contains i właściwość {{.Key}} mówią obie, co zawiera archiwum. Zostaw contains i usuń właściwość albo odwrotnie" }, + "format.ContentsConflict": { "hash": "sha256-f8af78b330b1", "other": "{{.Format}}: contains i właściwości {{.Keys}} mówią obie, jakie pliki zawiera. Zostaw contains i usuń właściwości albo odwrotnie" }, + "format.ContentsConflictOne": { "hash": "sha256-44b774ac5486", "other": "{{.Format}}: contains i właściwość {{.Key}} mówią obie, jakie pliki zawiera. Zostaw contains i usuń właściwość albo odwrotnie" }, "format.CountOf": { "hash": "sha256-8efd2508206c", "other": "{{.Number}} {{.Unit}}" }, "format.ExactlyOf": { "hash": "sha256-8efd2508206c", "other": "{{.Number}} {{.Unit}}" }, "format.GroupOf": { "hash": "sha256-72d3bb0ede42", "one": "{{.Count}} plik {{.Kind}} po {{.Bytes}} B", "few": "{{.Count}} pliki {{.Kind}} po {{.Bytes}} B", "many": "{{.Count}} plików {{.Kind}} po {{.Bytes}} B", "other": "{{.Count}} pliku {{.Kind}} po {{.Bytes}} B" }, @@ -275,7 +281,7 @@ "format.MixedPagesNeedAtLeastTwo": { "hash": "sha256-338f9f2bd35e", "other": "mieszane strony wymagają co najmniej dwóch stron, a ten dokument ma jedną" }, "format.NeitherTarNorGzipHasAny": { "hash": "sha256-b3831ecb57f7", "other": "ani tar, ani gzip nie mają żadnego szyfrowania, więc w żadnym z nich nie ma pola na hasło" }, "format.NeitherTarNorGzipHasAny2": { "hash": "sha256-92f4ef797f00", "other": "ani tar, ani gzip nie mają żadnego szyfrowania, więc nie ma między czym wybierać" }, - "format.NestingUnsupported": { "hash": "sha256-f2cd29c6111d", "other": "{{.Format}} nie może jeszcze zawierać {{.Inner}} - archiwum w archiwum wymaga najpierw limitu głębokości. Włóż inny format albo zbuduj wewnętrzne archiwum jako osobną partię" }, + "format.NestingUnsupported": { "hash": "sha256-f9a7add09c20", "other": "{{.Format}} nie może jeszcze zawierać {{.Inner}} - plik w pliku własnego formatu wymaga najpierw limitu głębokości. Włóż inny format albo zrób wewnętrzny plik osobną partią" }, "format.NoCalendarHasThatDayTime": { "hash": "sha256-4771df073166", "other": "żaden kalendarz nie ma takiego dnia, godziny ani strefy" }, "format.NoSuchProperty": { "hash": "sha256-d6b4359c3811", "other": "{{.Format}} nie ma właściwości o nazwie {{.Key}}" }, "format.NoSuchPropertyKnown": { "hash": "sha256-129d78741cc6", "other": "{{.What}}. Przyjmuje: {{.Known}}" }, diff --git a/internal/gui/text/screens.go b/internal/gui/text/screens.go index 39711c30..d23afce8 100644 --- a/internal/gui/text/screens.go +++ b/internal/gui/text/screens.go @@ -281,6 +281,7 @@ func ListNothingMatches() string { // reorders the headings with it rather than leaving them in English order. func ListKindArchives() string { return say("ListKindArchives", "Archives") } func ListKindDocuments() string { return say("ListKindDocuments", "Documents") } +func ListKindMail() string { return say("ListKindMail", "Mail") } func ListKindPictures() string { return say("ListKindPictures", "Pictures") } func ListKindSound() string { return say("ListKindSound", "Sound") } func ListKindText() string { return say("ListKindText", "Text and data") } diff --git a/internal/gui/window/recipe.go b/internal/gui/window/recipe.go index f512569a..01b65f9e 100644 --- a/internal/gui/window/recipe.go +++ b/internal/gui/window/recipe.go @@ -483,7 +483,7 @@ func (r *Recipe) batchBlock(index int, b *batch) fyne.CanvasObject { func (r *Recipe) contentsBlock(index int, b *batch) fyne.CanvasObject { holds := false if d, err := format.Get(b.formatPick.Selected); err == nil { - holds = d.Container + holds = d.Container != nil } addContents := parts.NewButton(parts.Secondary, text.ButtonAddContents(), func() { diff --git a/internal/oracle/mailscripts.go b/internal/oracle/mailscripts.go new file mode 100644 index 00000000..0bf0f839 --- /dev/null +++ b/internal/oracle/mailscripts.go @@ -0,0 +1,114 @@ +package oracle + +import ( + "os" + "strings" +) + +// pythonEmail is Python's email package with policy.default - the parser a +// Python service meets first, which is neither our code nor our language. +// +// It reads nearly anything, measured 2026-10-07 (docs/EML-2026-10-07.md +// section 5.1): of ten broken messages it refused none. What it does instead is +// list defects, and it listed one for every broken structure in the sample - a +// file cut short, a missing closing boundary, a stray character in base64, a +// boundary inside the text. So a defect is a refusal here, the way a warning is +// for GDAL and ffmpeg. What it says nothing about - a missing From line, a line +// past 998 characters - the format's guard asks strict.py. +var pythonEmail = Checker{ + Name: "python email", + find: inPath("python"), + args: func(p string) []string { return []string{"-c", pythonEmailScript, p, ""} }, + accept: expectOK("python email"), +} + +// PythonEmail is the same reader with defects it is told to expect. +// +// One exists today: a header written in UTF-8 as RFC 6532 allows, which the +// package reads correctly and still reports as UndecodableBytesDefect in the +// From and To lines (measured on the program's own messages, 2026-10-07). headers=utf8 asks for exactly that, so its guard passes the name +// rather than the checker passing every defect. +func PythonEmail(path string, allowed ...string) Result { + c := pythonEmail + c.args = func(p string) []string { + return []string{"-c", pythonEmailScript, p, strings.Join(allowed, ",")} + } + return c.Check(path) +} + +const pythonEmailScript = ` +import email, hashlib, json, sys +from email import policy + +# The names printed below are UTF-8 whatever the console's code page is. +sys.stdout.reconfigure(encoding="utf-8") +path, allowed = sys.argv[1], set(a for a in sys.argv[2].split(",") if a) +with open(path, "rb") as f: + msg = email.message_from_bytes(f.read(), policy=policy.default) + +defects = [] +for key in ("subject", "from", "to", "date", "message-id"): + value = msg.get(key) + if value is not None: + defects += ["%s:%s" % (key, type(d).__name__) for d in getattr(value, "defects", ())] + +parts = [] +for part in msg.walk(): + if part.is_multipart(): + continue + if part.get_content_disposition() == "attachment": + data = part.get_payload(decode=True) or b"" + parts.append({"name": part.get_filename(), "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}) + else: + part.get_content() +for part in msg.walk(): + defects += [type(d).__name__ for d in part.defects] + +unexpected = sorted(set(d for d in defects if d not in allowed)) +if unexpected: + print("FAIL defects: " + ", ".join(unexpected)) + sys.exit(1) +print("OK " + json.dumps({"subject": str(msg.get("subject")), "attachments": parts}, ensure_ascii=False)) +` + +// MailParser is mailparser, the parser behind nodemailer and much of the mail +// handling written in Node - the third language this format is read in. +// +// It lists no defects, measured on the same day: it read every broken message +// in silence, two of them with the wrong bytes. What it is here for is what it +// refuses that nobody else does - a message of more than a thousand MIME +// entities - and the names and bytes it reads back. Where it lives is the one +// thing node cannot find on its own: TFG_MAILPARSER names the installed +// package, and without it the guard skips loudly. +func MailParser(path string) Result { + module := os.Getenv("TFG_MAILPARSER") + return Checker{ + Name: "mailparser", + find: func() (string, bool) { + if module == "" { + return "", false + } + return inPath("node")() + }, + args: func(p string) []string { return []string{"-e", mailParserScript, module, p} }, + accept: expectOK("mailparser"), + }.Check(path) +} + +const mailParserScript = ` +const crypto = require("crypto"); +const fs = require("fs"); +const [module, path] = process.argv.slice(1); +const { simpleParser } = require(module); +simpleParser(fs.readFileSync(path)).then((m) => { + const attachments = m.attachments.map((a) => ({ + name: a.filename === undefined ? null : a.filename, + bytes: a.content.length, + sha256: crypto.createHash("sha256").update(a.content).digest("hex"), + })); + console.log("OK " + JSON.stringify({ subject: m.subject, attachments })); +}).catch((e) => { + console.log("FAIL " + e.message); + process.exit(1); +}); +` diff --git a/internal/oracle/oracle.go b/internal/oracle/oracle.go index bb7148d7..79841a26 100644 --- a/internal/oracle/oracle.go +++ b/internal/oracle/oracle.go @@ -351,6 +351,9 @@ var checkers = map[string]Checker{ // GDAL's GeoJSON driver. See geoscripts.go. "gdal-geojson": gdalGeoJSON, + + // Python's email package. See mailscripts.go. + "python-email": pythonEmail, } // DecodedFrames is how many frames the libaom oracle decoded, read from what @@ -541,6 +544,9 @@ func StrictKnows(formatID string) bool { // From its first day, because GDAL alone took files this one refuses // (docs/GEOJSON-2026-10-07.md section 3). "geojson", + // The same reason for mail: four readers took a message with no From + // line (docs/EML-2026-10-07.md section 5.1). + "eml", "bmp", "gif", "ico", "jpg", "tiff", "webp", "avif", "jxl", "docx", "xlsx", "pptx", // The two text formats joined on 2026-09-07, when they gained an // encoding. Before that there was nothing here to check against diff --git a/internal/oracle/strict.py b/internal/oracle/strict.py index d24b4ae0..686bad24 100644 --- a/internal/oracle/strict.py +++ b/internal/oracle/strict.py @@ -2550,6 +2550,276 @@ def geo_collection_box(doc, boxes, located, settings): geo_bbox("the collection", doc["bbox"], lo, hi, settings["precision"]) +EML_INTERNATIONAL = "Zażółć gęślą jaźń. 日本語のテキストです。" +EML_WIDE_PREFIX = "Załącznik_日本語_" +EML_BASE64_LINE = re.compile(rb"^[A-Za-z0-9+/]+={0,2}$") +EML_NAME = re.compile(r"^(?:" + EML_WIDE_PREFIX + r")?[a-z0-9]+_\d{4}\.[a-z0-9.]+$") + + +def check_eml(data, settings=None): + """A mail message to RFC 5322 and MIME, made the way it was ordered. + + Measured 2026-10-07 (docs/EML-2026-10-07.md section 5.1): Python's email + package, mailparser, MimeKit and Go's standard library took a message with + no From line, a header line of 2000 characters and a line of base64 13 336 + long without a word, and two of them returned the wrong bytes of a broken + attachment in silence. What only this layer asks: the header lines in their + order and once each, no line past 998 characters and none past 78 but a + media type too long to fold, every line ending the way line_endings says, + the boundaries nowhere inside a part, the text in the encoding and the + character set ordered with the same words in both parts of alternative, + base64 in full lines of 76, and every name written where and how + filename_style and headers say. + """ + given = settings or {} + s = {k: given.get(k, v) for k, v in ( + ("body", "plain"), ("text_encoding", "7bit"), ("headers", "ascii"), + ("line_endings", "crlf"), ("filename_style", "rfc2231"), ("attachments", "0"))} + eol = b"\r\n" if s["line_endings"] == "crlf" else b"\n" + eml_line_endings(data, eol) + for number, line in enumerate(data.split(eol), start=1): + eml_line_length(number, line) + # A message this tool did not write can break the walk in ways nobody + # listed - bytes that are not UTF-8, a part with no Content-Type, base64 + # that does not decode. Each has to come out as a refusal with a reason, + # not a traceback, because a crash turns a guard red and says nothing + # (the lesson of check_geojson, review of #171, asked again in #173). + leaves = [] + try: + head, body = eml_split(data, eol, "the message") + fields = eml_fields(head, eol, "the message") + eml_headers(fields, s) + eml_entity(dict(fields), body, eol, s, leaves, top=True) + except (UnicodeDecodeError, KeyError, ValueError, IndexError, TypeError) as exc: + fail(f"a part is not shaped the way this tool writes one: {type(exc).__name__}: {exc}") + texts = [leaf for leaf in leaves if leaf["kind"] == "text"] + files = [leaf for leaf in leaves if leaf["kind"] == "file"] + want_texts = {"plain": ["text/plain"], "html": ["text/html"], "alternative": ["text/plain", "text/html"]}[s["body"]] + if [leaf["type"] for leaf in texts] != want_texts: + fail(f"the text parts are {[leaf['type'] for leaf in texts]}, and body={s['body']} makes {want_texts}") + if len(texts) == 2 and eml_words(texts[0]["text"]) != eml_words(re.sub(r"<[^>]*>", " ", texts[1]["text"])): + fail("the two parts of alternative do not carry the same words") + if len(files) != int(s["attachments"]): + fail(f"{len(files)} attached files, and {s['attachments']} were ordered") + ok(f"{len(files)} attached files, {sum(len(f['bytes']) for f in files)} B of them, body {s['body']}, " + f"text {s['text_encoding']}, headers {s['headers']}, names {s['filename_style']}") + + +def eml_line_endings(data, eol): + """Every line ends in eol and in nothing else - no lone carriage return, and + no line feed without its return when the endings are crlf.""" + if eol == b"\n": + if b"\r" in data: + fail("a carriage return in a message whose lines end in lf") + return + if re.search(rb"\r(?!\n)|(? 998: + fail(f"line {number} is {len(text)} characters, past the 998 RFC 5322 allows") + if len(text) > 78 and not re.fullmatch(r"Content-Type: [a-z]+/[a-z0-9.+-]+;?", text): + fail(f"line {number} is {len(text)} characters, past the 78 RFC 5322 asks for: {text[:60]!r}") + + +def eml_split(data, eol, where): + """The header lines of an entity and what follows the blank line after them.""" + cut = data.find(eol + eol) + if cut < 0: + fail(f"{where} has no blank line after its header lines") + return data[:cut], data[cut + 2 * len(eol):] + + +def eml_fields(head, eol, where): + """The header fields, unfolded, as (name, value) in their order.""" + out = [] + for line in head.split(eol): + if line[:1] in (b" ", b"\t"): + if not out: + fail(f"{where} starts with a folded line") + out[-1] = (out[-1][0], out[-1][1] + line.decode("utf-8")) + continue + name, sep, value = line.decode("utf-8").partition(": ") + if not sep: + fail(f"{where} has a header line with no name: {line[:40]!r}") + out.append((name, value)) + return out + + +def eml_headers(fields, s): + """The lines a message opens with, in order and once each - From and Date + are the two RFC 5322 requires - and written the way headers says.""" + import email.utils + + names = [name for name, _ in fields] + want = ["From", "To", "Date", "Message-ID", "Subject", "MIME-Version", "Content-Type"] + if names[:7] != want or names[7:] not in ([], ["Content-Transfer-Encoding"]): + fail(f"the message opens with {names}, not {want}") + values = dict(fields) + if values["MIME-Version"] != "1.0": + fail(f"MIME-Version is {values['MIME-Version']!r}") + if not re.fullmatch(r"<[0-9a-f]{16}@example\.com>", values["Message-ID"]): + fail(f"the Message-ID {values['Message-ID']!r} is not the one this tool writes") + if email.utils.parsedate_to_datetime(values["Date"]) is None: + fail(f"the Date {values['Date']!r} is not an RFC 5322 date") + people = " ".join(values[k] for k in ("From", "To", "Subject")) + wide = any(ord(c) > 127 for c in people) + words = "=?UTF-8?B?" in people + if (s["headers"], wide, words) not in (("ascii", False, False), ("encoded", False, True), ("utf8", True, False)): + fail(f"headers={s['headers']} and the From, To and Subject lines hold " + f"{'letters outside ASCII' if wide else 'ASCII only'}{' and encoded words' if words else ''}") + for key, address in (("From", "alice@example.com"), ("To", "bob@example.org")): + if not values[key].endswith(" <" + address + ">"): + fail(f"{key} is {values[key]!r}, not an address in a domain RFC 2606 keeps for examples") + + +def eml_params(value): + """A header value as its first word and its parameters, quotes kept.""" + parts = re.findall(r'(?:[^;"]|"[^"]*")+', value) + params = {} + for p in parts[1:]: + key, _, v = p.strip().partition("=") + params[key] = v + return parts[0].strip(), params + + +def eml_entity(headers, body, eol, s, leaves, top): + """One entity: a multipart one walked part by part, a leaf judged as text or + as an attached file.""" + kind, params = eml_params(headers["Content-Type"]) + if not kind.startswith("multipart/"): + leaves.append(eml_text(kind, params, headers, body, s) if "Content-Disposition" not in headers + else eml_file(kind, params, headers, body, eol, s)) + return + boundary = params.get("boundary", "").strip('"').encode() + if not boundary.startswith(b"=_"): + fail(f"the boundary {boundary!r} is not one that base64 and quoted-printable cannot produce") + opener, close = b"--" + boundary + eol, eol + b"--" + boundary + b"--" + end = body.rfind(close) + if not body.startswith(opener) or end < 0: + fail(f"{kind} does not open with its boundary or has no closing one") + if body[end + len(close):] != (eol if top else b""): + fail(f"{kind} has something after its closing boundary") + for piece in body[len(opener):end].split(eol + b"--" + boundary + eol): + if any(line.startswith(b"--" + boundary) for line in piece.split(eol)): + fail(f"the boundary of {kind} appears inside one of its parts") + part_head, part_body = eml_split(piece, eol, f"a part of {kind}") + eml_entity(dict(eml_fields(part_head, eol, f"a part of {kind}")), part_body, eol, s, leaves, top=False) + + +def eml_text(kind, params, headers, body, s): + """A text part in the character set and the encoding text_encoding orders, + with the line in Polish and Japanese exactly when it is not 7bit.""" + charset = "us-ascii" if s["text_encoding"] == "7bit" else "utf-8" + if params.get("charset") != charset or headers.get("Content-Transfer-Encoding") != s["text_encoding"]: + fail(f"a {kind} part says charset {params.get('charset')} in {headers.get('Content-Transfer-Encoding')}, " + f"and text_encoding={s['text_encoding']} makes {charset} in {s['text_encoding']}") + eol = "\r\n" if s["line_endings"] == "crlf" else "\n" + if s["text_encoding"] == "quoted-printable": + text = eml_unquote(body, eol.encode()) + else: + text = body.decode("utf-8" if s["text_encoding"] == "8bit" else "ascii") + if (EML_INTERNATIONAL in text) != (s["text_encoding"] != "7bit"): + fail(f"the {kind} part {'lacks' if s['text_encoding'] != '7bit' else 'carries'} the line in Polish and Japanese") + for line in text.split(eol): + if line.startswith(("From ", "--")) or line == ".": + fail(f"a line of the {kind} part starts the way a boundary, an mbox file or SMTP reads: {line[:20]!r}") + return {"kind": "text", "type": kind, "text": text} + + +def eml_unquote(body, eol): + """Quoted-printable decoded, after the lines are checked against RFC 2045 + section 6.7: at most 76 characters, no space at the end, no = that is not + a soft break or two upper case hexadecimal digits.""" + out = bytearray() + for line in body.split(eol): + if len(line) > 76 or line[-1:] in (b" ", b"\t"): + fail(f"a quoted-printable line is {len(line)} characters or ends in white space") + soft = line.endswith(b"=") + line = line[:-1] if soft else line + if re.search(rb"=(?![0-9A-F]{2})", line): + fail(f"a quoted-printable line holds an = that is not =XX: {line[:40]!r}") + out += re.sub(rb"=([0-9A-F]{2})", lambda m: bytes([int(m.group(1), 16)]), line) + if not soft: + out += eol + return bytes(out[: -len(eol)]).decode("utf-8") + + +def eml_words(text): + return re.findall(r"[^\s<>]+", text) + + +def eml_file(kind, params, headers, body, eol, s): + """An attached file: base64 in full lines of 76, and its name where and how + filename_style and headers put it.""" + disposition, dparams = eml_params(headers["Content-Disposition"]) + if disposition != "attachment" or headers.get("Content-Transfer-Encoding") != "base64": + fail(f"an attached {kind} file is {disposition} in {headers.get('Content-Transfer-Encoding')}, not attachment in base64") + lines = body.split(eol) if body else [] + for i, line in enumerate(lines): + if not EML_BASE64_LINE.match(line) or (i < len(lines) - 1 and len(line) != 76) or len(line) > 76: + fail(f"line {i + 1} of an attached {kind} file is not a line of base64 76 long: {line[:40]!r}") + import base64 + + data = base64.b64decode(b"".join(lines), validate=True) + name = eml_file_name(kind, params, dparams, s) + if not EML_NAME.match(name) or name.startswith(EML_WIDE_PREFIX) != (s["headers"] != "ascii"): + fail(f"an attached file is named {name!r}, not the way headers={s['headers']} names one") + return {"kind": "file", "type": kind, "name": name, "bytes": data} + + +def eml_file_name(kind, params, dparams, s): + """The name of an attached file, read from where filename_style says it is + and nowhere else, decoded the way it says it is written.""" + import base64 + from urllib.parse import unquote + + style, encoded = s["filename_style"], s["headers"] == "encoded" + in_type = "name" in params + in_disposition = sorted(k for k in dparams if k.startswith("filename")) + want_type = style != "rfc2231" + if in_type != want_type or bool(in_disposition) == (style == "content-type"): + fail(f"filename_style={style} and the name is {'in' if in_type else 'not in'} Content-Type and " + f"{'in' if in_disposition else 'not in'} Content-Disposition") + names = [] + if in_type: + names.append(eml_quoted(params["name"], encoded)) + if in_disposition == ["filename"]: + names.append(eml_quoted(dparams["filename"], encoded and style == "rfc2047")) + elif in_disposition: + if not encoded or style == "rfc2047": + fail(f"filename_style={style} with headers={s['headers']} writes the name in RFC 2231 form") + joined = "".join(dparams[k] for k in sorted(in_disposition, key=lambda k: int(re.sub(r"\D", "", k) or 0))) + if not joined.startswith("UTF-8''"): + fail(f"an RFC 2231 name does not say UTF-8: {joined[:30]!r}") + names.append(unquote(joined[len("UTF-8''"):], encoding="utf-8", errors="strict")) + if len(set(names)) != 1: + fail(f"the attached {kind} file has two names: {names}") + return names[0] + + +def eml_quoted(value, encoded): + """A name in quotes, as it is or as RFC 2047 words when encoded is true.""" + import base64 + + if not (value.startswith('"') and value.endswith('"')): + fail(f"a name is not in quotes: {value[:40]!r}") + inner = value[1:-1] + words = re.findall(r"=\?UTF-8\?B\?([A-Za-z0-9+/=]+)\?=", inner) + if not encoded: + if words: + fail(f"a name is in encoded words that headers does not ask for: {inner[:40]!r}") + return inner + if not words or re.sub(r"=\?UTF-8\?B\?[A-Za-z0-9+/=]+\?=", "", inner).strip(): + fail(f"a name is not encoded words and nothing else: {inner[:40]!r}") + return b"".join(base64.b64decode(w) for w in words).decode("utf-8") + + CHECKS = {"png": check_png, "wav": check_wav, "pdf": check_pdf, "zip": check_zip, "log": check_log, "csv": check_csv, "json": check_json, "xml": check_xml, "svg": check_svg, "html": check_html, "targz": check_targz, @@ -2558,12 +2828,13 @@ def geo_collection_box(doc, boxes, located, settings): "webm": check_webm, "mp4": check_mp4, "docx": check_docx, "xlsx": check_xlsx, "pptx": check_pptx, "txt": check_txt, "md": check_md, - "yaml": check_yaml, "toml": check_toml, "geojson": check_geojson} + "yaml": check_yaml, "toml": check_toml, "geojson": check_geojson, + "eml": check_eml} # Checks that take the shape of the file as well as its bytes. Everything else # is handed the bytes alone, so adding a setting to one check cannot change how # any other one is called. -TAKES_SETTINGS = {"csv", "txt", "md", "json", "xml", "html", "geojson"} +TAKES_SETTINGS = {"csv", "txt", "md", "json", "xml", "html", "geojson", "eml"} if __name__ == "__main__": if len(sys.argv) < 3 or sys.argv[1] not in CHECKS: diff --git a/web/assets/social-preview.png b/web/assets/social-preview.png index 341a793b..88c924b1 100644 Binary files a/web/assets/social-preview.png and b/web/assets/social-preview.png differ diff --git a/web/public/ar/corrupt-test-files/index.html b/web/public/ar/corrupt-test-files/index.html index 609cede1..fccd0d48 100644 --- a/web/public/ar/corrupt-test-files/index.html +++ b/web/public/ar/corrupt-test-files/index.html @@ -198,7 +198,7 @@

الملف التالف يبقى بالحجم الذي طلبته

tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

تُكتب الإعدادات بعد النقطتين. يمكن تكرار الخيار، وتُطبَّق أنواع التلف بالترتيب الذي تكتبه. ويعمل مع - كل واحدة من 29 صيغة. + كل واحدة من 30 صيغة.

diff --git a/web/public/ar/faq/index.html b/web/public/ar/faq/index.html index d5bfeb74..a1568437 100644 --- a/web/public/ar/faq/index.html +++ b/web/public/ar/faq/index.html @@ -205,7 +205,7 @@

الأسئلة الشائعة

ما الصيغ القادمة؟

-

7z وmp3. تعمل اليوم 29 صيغة من البداية إلى النهاية.

+

7z وmp3. تعمل اليوم 30 صيغة من البداية إلى النهاية.

@@ -276,7 +276,7 @@

الأسئلة الشائعة

{ "@type": "Question", "name": "ما الصيغ القادمة؟", - "acceptedAnswer": { "@type": "Answer", "text": "7z وmp3. تعمل اليوم 29 صيغة من البداية إلى النهاية." } + "acceptedAnswer": { "@type": "Answer", "text": "7z وmp3. تعمل اليوم 30 صيغة من البداية إلى النهاية." } }, { "@type": "Question", diff --git a/web/public/ar/formats/index.html b/web/public/ar/formats/index.html index d5f11500..a90ded10 100644 --- a/web/public/ar/formats/index.html +++ b/web/public/ar/formats/index.html @@ -4,8 +4,8 @@ -29 صيغة ملف مدعومة - PDF وDOCX وPNG وZIP وغيرها - +30 صيغة ملف مدعومة - PDF وDOCX وPNG وZIP وغيرها + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
-

29 صيغة ملف، كلٌّ منها يُولَّد بحجم دقيق

+

30 صيغة ملف، كلٌّ منها يُولَّد بحجم دقيق

كلٌّ منها ملف حقيقي من تلك الصيغة. يُفتح في البرنامج الذي يملكه، وعدد بايتاته هو ما طلبته بالضبط. ولا واحد منها أصفار حشوية أُلصق بها امتداد. @@ -195,6 +195,14 @@

29 صيغة ملف، كلٌّ منها يُولَّد بحجم دقيق

full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -503,6 +511,46 @@

الإعدادات التي تقبلها كل صيغة

paragraphs 1 - 50000 فقرات + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + معرّف صيغة، كما يسردها tfg formats + + + + attachment_size + حجم مثل 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1004,7 +1052,7 @@

الإعدادات التي تقبلها كل صيغة

الأرشيفات تحتوي ملفات حقيقية

- targz وzip يمكن + eml وtargz وzip يمكن ملؤها بعناصر بدل تركها قشرة فارغة. الأرشيف المولَّد يحتوي فعلًا على المستندات التي يدّعي احتواءها، فكل ما يفكّه أثناء اختبار يجد بداخله ملفات حقيقية.

diff --git a/web/public/ar/index.html b/web/public/ar/index.html index e69c2b01..bf807768 100644 --- a/web/public/ar/index.html +++ b/web/public/ar/index.html @@ -4,7 +4,7 @@ -مولّد ملفات الاختبار لضمان الجودة - حجم دقيق، 29 صيغة حقيقية +مولّد ملفات الاختبار لضمان الجودة - حجم دقيق، 30 صيغة حقيقية @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "مولّد ملفات اختبار مجاني ومفتوح المصدر لضمان الجودة. ينتج ملفات حقيقية بـ 29 صيغة وبحجم دقيق، ويكتب بيانًا يحدد كيف يجب أن يتفاعل النظام قيد الاختبار مع كل ملف.", + "description": "مولّد ملفات اختبار مجاني ومفتوح المصدر لضمان الجودة. ينتج ملفات حقيقية بـ 30 صيغة وبحجم دقيق، ويكتب بيانًا يحدد كيف يجب أن يتفاعل النظام قيد الاختبار مع كل ملف.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

ولّد ملفات اختبار حقيقية بحجم دقيق

- PDF وPNG وDOCX وZIP، 29 صيغة في المجمل، وكلٌّ منها ملف حقيقي + PDF وPNG وDOCX وZIP، 30 صيغة في المجمل، وكلٌّ منها ملف حقيقي يُفتح في البرنامج الذي يملكه، وبـالحجم الذي طلبته تمامًا. كما يدوّن كل تشغيل ما يجب أن يفعله تطبيقك بكل ملف. سطر أوامر ونافذة سطح مكتب، مجاني ومفتوح المصدر، ويعمل بالكامل على جهازك. @@ -179,7 +179,7 @@

ولّد ملفات اختبار حقيقية بحجم دقيق

  • - 29 + 30

    صيغة حقيقية، وكلٌّ منها يُفتح في البرنامج الذي يملكه

  • @@ -340,7 +340,7 @@

    مبني لمجموعة اختبارات تعمل دون إشراف

    اطلب 10485761 بايتًا واحصل على هذا العدد بالضبط. الحجم الذي لا تستطيع الصيغة بلوغه خطأ مع سبب، وليس ملفًا بحجم خاطئ أبدًا.

  • -

    29 صيغة حقيقية

    +

    30 صيغة حقيقية

    ليست أصفارًا حشوية بامتداد. يُفتح ملف PNG المولَّد في عارض الصور، ويُفتح DOCX في Word، ويُفك ضغط ZIP. وتُفحص كل صيغة بقرّاء مستقلين قبل إصدارها.

  • diff --git a/web/public/ar/presets/empty-and-minimal/index.html b/web/public/ar/presets/empty-and-minimal/index.html index 6a825470..e3ffba68 100644 --- a/web/public/ar/presets/empty-and-minimal/index.html +++ b/web/public/ar/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ أصغر ملفات اختبار صالحة وفارغة في كل صيغة - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

    ماذا في المجموعة؟

    - - - - + + + +
    الملفات31
    الأهداف في وصفته31
    الحجم الإجمالي40 138 B
    الصيغavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
    الملفات32
    الأهداف في وصفته32
    الحجم الإجمالي40 427 B
    الصيغavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
    @@ -192,7 +192,7 @@

    ماذا في المجموعة؟

    المتوقعالمعنىالملفات - acceptينبغي أن يقبل نظامك الملف.29 + acceptينبغي أن يقبل نظامك الملف.30 unspecifiedيعتمد على قواعد نظامك. أنت من يقرر، ثم تتحقق من أن ما يحدث هو ما قصدته.2 diff --git a/web/public/assets/social-preview.png b/web/public/assets/social-preview.png index 341a793b..88c924b1 100644 Binary files a/web/public/assets/social-preview.png and b/web/public/assets/social-preview.png differ diff --git a/web/public/corrupt-test-files/index.html b/web/public/corrupt-test-files/index.html index c52528c8..5061535d 100644 --- a/web/public/corrupt-test-files/index.html +++ b/web/public/corrupt-test-files/index.html @@ -201,7 +201,7 @@

    A damaged file is still the size you asked for

    tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

    Settings go after a colon. The flag can be repeated, and the damages are applied in the order you - write them. It works with every one of the 29 formats. + write them. It works with every one of the 30 formats.

diff --git a/web/public/cs/faq/index.html b/web/public/cs/faq/index.html index fdb042d2..b8d75221 100644 --- a/web/public/cs/faq/index.html +++ b/web/public/cs/faq/index.html @@ -206,7 +206,7 @@

Často kladené otázky

Které formáty přijdou dál?

-

7z a mp3. Dnes funguje od začátku do konce 29 formátů.

+

7z a mp3. Dnes funguje od začátku do konce 30 formátů.

@@ -277,7 +277,7 @@

Často kladené otázky

{ "@type": "Question", "name": "Které formáty přijdou dál?", - "acceptedAnswer": { "@type": "Answer", "text": "7z a mp3. Dnes funguje od začátku do konce 29 formátů." } + "acceptedAnswer": { "@type": "Answer", "text": "7z a mp3. Dnes funguje od začátku do konce 30 formátů." } }, { "@type": "Question", diff --git a/web/public/cs/formaty/index.html b/web/public/cs/formaty/index.html index 9328d60f..4a2f41ae 100644 --- a/web/public/cs/formaty/index.html +++ b/web/public/cs/formaty/index.html @@ -4,8 +4,8 @@ -29 podporovaných formátů souborů - PDF, DOCX, PNG, ZIP a další - +30 podporovaných formátů souborů - PDF, DOCX, PNG, ZIP a další + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
-

29 formátů souborů, každý generovaný v přesné velikosti

+

30 formátů souborů, každý generovaný v přesné velikosti

Každý z nich je skutečný soubor toho formátu. Otevře se v programu, kam patří, a má přesně tolik bajtů, kolik jste požádali. Žádný není vycpávka z nul s přilepenou příponou. @@ -195,6 +195,14 @@

29 formátů souborů, každý generovaný v přesné velikosti

full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -504,6 +512,46 @@

Nastavení, která každý formát přijímá

paragraphs 1 - 50000 odstavců + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + id formátu tak, jak je vypisuje tfg formats + + + + attachment_size + velikost, například 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1006,7 +1054,7 @@

Nastavení, která každý formát přijímá

Archivy obsahují skutečné soubory

- targz a zip lze + eml a targz a zip lze naplnit položkami, místo aby zůstaly prázdnou skořápkou. Vygenerovaný archiv skutečně obsahuje dokumenty, které tvrdí, že obsahuje, takže cokoli ho během testu rozbalí, uvnitř najde skutečné soubory. diff --git a/web/public/cs/index.html b/web/public/cs/index.html index 582bd83b..0c16aad0 100644 --- a/web/public/cs/index.html +++ b/web/public/cs/index.html @@ -4,7 +4,7 @@ -Generátor testovacích souborů - přesná velikost, 29 formátů +Generátor testovacích souborů - přesná velikost, 30 formátů @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Bezplatný open source generátor testovacích souborů pro QA. Vytváří skutečné soubory v 29 formátech v přesné velikosti a zapisuje manifest, který říká, jak má testovaný systém na každý z nich reagovat.", + "description": "Bezplatný open source generátor testovacích souborů pro QA. Vytváří skutečné soubory v 30 formátech v přesné velikosti a zapisuje manifest, který říká, jak má testovaný systém na každý z nich reagovat.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

Generujte skutečné testovací soubory v přesné velikosti

- PDF, PNG, DOCX, ZIP - celkem 29 formátů a každý je skutečný + PDF, PNG, DOCX, ZIP - celkem 30 formátů a každý je skutečný soubor, který se otevře v programu, kam patří, v přesně té velikosti, o kterou jste požádali. Každý běh také zapisuje, co má vaše aplikace s každým souborem udělat. Příkazový řádek a desktopové okno, zdarma a open source, celé na vašem počítači. @@ -179,7 +179,7 @@

Generujte skutečné testovací soubory v přesné velikosti

  • - 29 + 30

    skutečných formátů, každý se otevře v programu, kam patří

  • @@ -341,7 +341,7 @@

    Vytvořeno pro sadu testů, která běží bez dozoru

    Požádejte o 10485761 bajtů a dostanete přesně tolik. Velikost, které formát nedosáhne, je chyba s důvodem, nikdy soubor špatné velikosti.

  • -

    29 skutečných formátů

    +

    30 skutečných formátů

    Ne vycpávkové nuly s příponou. Vygenerované PNG se otevře v prohlížeči obrázků, DOCX ve Wordu, ZIP se rozbalí. Každý formát je před vydáním ověřen nezávislými čtečkami.

  • diff --git a/web/public/cs/poskozene-testovaci-soubory/index.html b/web/public/cs/poskozene-testovaci-soubory/index.html index e88cf87a..0c9691a0 100644 --- a/web/public/cs/poskozene-testovaci-soubory/index.html +++ b/web/public/cs/poskozene-testovaci-soubory/index.html @@ -200,7 +200,7 @@

    Poškozený soubor má stále velikost, kterou jste chtěli

    tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

    Nastavení se píše za dvojtečku. Přepínač lze opakovat a poškození se použijí v pořadí, v jakém je - zapíšete. Funguje s každým z 29 formátů. + zapíšete. Funguje s každým z 30 formátů.

diff --git a/web/public/cs/predvolby/empty-and-minimal/index.html b/web/public/cs/predvolby/empty-and-minimal/index.html index 7bd23693..9f49306b 100644 --- a/web/public/cs/predvolby/empty-and-minimal/index.html +++ b/web/public/cs/predvolby/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Nejmenší platné a prázdné testovací soubory v každém formátu - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

Co je v sadě?

- - - - + + + +
Soubory31
Targety v receptu31
Celková velikost40 138 B
Formátyavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
Soubory32
Targety v receptu32
Celková velikost40 427 B
Formátyavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
@@ -192,7 +192,7 @@

Co je v sadě?

OčekávánoVýznamSoubory - acceptVáš systém má soubor přijmout.29 + acceptVáš systém má soubor přijmout.30 unspecifiedZáleží na pravidlech vašeho systému. Rozhodnete vy, a pak ověříte, že to, co se stane, je to, co jste chtěli.2 diff --git a/web/public/de/beschaedigte-testdateien/index.html b/web/public/de/beschaedigte-testdateien/index.html index c2d28324..cd61d4eb 100644 --- a/web/public/de/beschaedigte-testdateien/index.html +++ b/web/public/de/beschaedigte-testdateien/index.html @@ -203,7 +203,7 @@

Eine beschädigte Datei hat trotzdem die verlangte Größe

Einstellungen stehen nach einem Doppelpunkt. Die Option lässt sich wiederholen, und die Beschädigungen werden in der Reihenfolge angewendet, in der Sie sie schreiben. Es funktioniert - mit allen 29 Formaten. + mit allen 30 Formaten.

diff --git a/web/public/de/faq/index.html b/web/public/de/faq/index.html index 4a95c26f..62890443 100644 --- a/web/public/de/faq/index.html +++ b/web/public/de/faq/index.html @@ -206,7 +206,7 @@

Häufig gestellte Fragen

Welche Formate kommen als Nächstes?

-

7z und mp3. 29 Formate funktionieren heute durchgängig.

+

7z und mp3. 30 Formate funktionieren heute durchgängig.

@@ -277,7 +277,7 @@

Häufig gestellte Fragen

{ "@type": "Question", "name": "Welche Formate kommen als Nächstes?", - "acceptedAnswer": { "@type": "Answer", "text": "7z und mp3. 29 Formate funktionieren heute durchgängig." } + "acceptedAnswer": { "@type": "Answer", "text": "7z und mp3. 30 Formate funktionieren heute durchgängig." } }, { "@type": "Question", diff --git a/web/public/de/formate/index.html b/web/public/de/formate/index.html index 50b74c19..06b6edec 100644 --- a/web/public/de/formate/index.html +++ b/web/public/de/formate/index.html @@ -4,8 +4,8 @@ -29 unterstützte Dateiformate - PDF, DOCX, PNG, ZIP und mehr - +30 unterstützte Dateiformate - PDF, DOCX, PNG, ZIP und mehr + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
-

29 Dateiformate, jedes in exakter Größe erzeugt

+

30 Dateiformate, jedes in exakter Größe erzeugt

Jedes davon ist eine echte Datei dieses Formats. Sie öffnet sich in der Software, zu der sie gehört, und hat genau die Byte-Zahl, die du verlangt hast. Keines davon besteht aus @@ -196,6 +196,14 @@

29 Dateiformate, jedes in exakter Größe erzeugt

full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -506,6 +514,46 @@

Einstellungen, die jedes Format kennt

paragraphs 1 - 50000 Absätze + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + die ID eines Formats, wie tfg formats sie auflistet + + + + attachment_size + eine Größe wie 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1010,7 +1058,7 @@

Einstellungen, die jedes Format kennt

Archive enthalten echte Dateien

- targz und zip + eml und targz und zip lassen sich mit Einträgen füllen, statt als leere Hülle zu bleiben. Ein erzeugtes Archiv enthält wirklich die Dokumente, die es zu enthalten vorgibt, sodass alles, was es während eines Tests entpackt, echte Dateien darin findet. diff --git a/web/public/de/index.html b/web/public/de/index.html index c130da0e..3e5f1832 100644 --- a/web/public/de/index.html +++ b/web/public/de/index.html @@ -4,7 +4,7 @@ -Testdatei-Generator für QA - exakte Größe, 29 echte Formate +Testdatei-Generator für QA - exakte Größe, 30 echte Formate @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Ein kostenloser Open-Source-Generator für Testdateien in der QA. Er erzeugt echte Dateien in 29 Formaten in jeder exakten Größe und schreibt ein Manifest, das beschreibt, wie das getestete System auf jede Datei reagieren soll.", + "description": "Ein kostenloser Open-Source-Generator für Testdateien in der QA. Er erzeugt echte Dateien in 30 Formaten in jeder exakten Größe und schreibt ein Manifest, das beschreibt, wie das getestete System auf jede Datei reagieren soll.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

Echte Testdateien in jeder exakten Größe erzeugen

- PDF, PNG, DOCX, ZIP - insgesamt 29 Formate, und jedes ist + PDF, PNG, DOCX, ZIP - insgesamt 30 Formate, und jedes ist eine echte Datei, die sich in der Software öffnet, zu der sie gehört, in genau der Größe, die du verlangt hast. Jeder Lauf hält außerdem fest, was deine Anwendung mit jeder Datei tun soll. Kommandozeile und Desktop-Fenster, kostenlos und Open Source, vollständig @@ -180,7 +180,7 @@

Echte Testdateien in jeder exakten Größe erzeugen

  • - 29 + 30

    echte Formate, jedes öffnet sich in der Software, zu der es gehört

  • @@ -345,7 +345,7 @@

    Gebaut für eine Testsuite, die unbeaufsichtigt läuft

    Verlange 10485761 Bytes und bekomme genau das. Eine Größe, die ein Format nicht erreichen kann, ist ein Fehler mit Begründung, nie eine Datei in der falschen Größe.

  • -

    29 echte Formate

    +

    30 echte Formate

    Keine aufgefüllten Nullen mit einer Endung. Ein erzeugtes PNG öffnet sich im Bildbetrachter, ein DOCX in Word, ein ZIP lässt sich entpacken. Jedes wird vor der Auslieferung mit unabhängigen Readern geprüft.

  • diff --git a/web/public/de/presets/empty-and-minimal/index.html b/web/public/de/presets/empty-and-minimal/index.html index ac25dc11..81fb5dbf 100644 --- a/web/public/de/presets/empty-and-minimal/index.html +++ b/web/public/de/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Kleinste gültige und leere Testdateien in jedem Format - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

    Was steckt im Set?

    - - - - + + + +
    Dateien31
    Targets in seinem Rezept31
    Gesamtgröße40 138 B
    Formateavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
    Dateien32
    Targets in seinem Rezept32
    Gesamtgröße40 427 B
    Formateavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
    @@ -192,7 +192,7 @@

    Was steckt im Set?

    ErwartetBedeutungDateien - acceptDein System soll die Datei annehmen.29 + acceptDein System soll die Datei annehmen.30 unspecifiedDas hängt von den Regeln deines Systems ab. Du entscheidest, dann prüfst du, ob das Ergebnis dem entspricht, was du gemeint hast.2 diff --git a/web/public/es/archivos-de-prueba-corruptos/index.html b/web/public/es/archivos-de-prueba-corruptos/index.html index c93e4676..e41dfb87 100644 --- a/web/public/es/archivos-de-prueba-corruptos/index.html +++ b/web/public/es/archivos-de-prueba-corruptos/index.html @@ -201,7 +201,7 @@

    Un archivo dañado conserva el tamaño que pediste

    tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

    Los ajustes van después de dos puntos. La opción se puede repetir, y los daños se aplican en el - orden en que los escribes. Funciona con cada uno de los 29 formatos. + orden en que los escribes. Funciona con cada uno de los 30 formatos.

diff --git a/web/public/es/formatos/index.html b/web/public/es/formatos/index.html index 068887ec..f23bc473 100644 --- a/web/public/es/formatos/index.html +++ b/web/public/es/formatos/index.html @@ -4,8 +4,8 @@ -29 formatos de archivo - PDF, DOCX, PNG, ZIP y más - +30 formatos de archivo - PDF, DOCX, PNG, ZIP y más + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
-

29 formatos de archivo, cada uno generado a un tamaño exacto

+

30 formatos de archivo, cada uno generado a un tamaño exacto

Cada uno es un archivo real de ese formato. Se abre en el programa que le corresponde y tiene exactamente el número de bytes que pediste. Ninguno es relleno de ceros con @@ -196,6 +196,14 @@

29 formatos de archivo, cada uno generado a un tamaño exacto

full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -507,6 +515,46 @@

Ajustes que admite cada formato

paragraphs 1 - 50000 párrafos + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + el id de un formato, tal como lo lista tfg formats + + + + attachment_size + un tamaño como 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1010,7 +1058,7 @@

Ajustes que admite cada formato

Los archivos comprimidos contienen archivos reales

- targz y zip se + eml y targz y zip se pueden llenar de entradas en lugar de quedar como un cascarón vacío. Un archivo comprimido generado contiene de verdad los documentos que dice contener, así que todo lo que lo descomprima durante una prueba encuentra archivos reales dentro. diff --git a/web/public/es/index.html b/web/public/es/index.html index 07078c21..be15f7c0 100644 --- a/web/public/es/index.html +++ b/web/public/es/index.html @@ -4,7 +4,7 @@ -Generador de archivos de prueba - tamaño exacto, 29 formatos +Generador de archivos de prueba - tamaño exacto, 30 formatos @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Un generador gratuito y de código abierto de archivos de prueba para QA. Produce archivos reales de 29 formatos al tamaño exacto y escribe un manifiesto que dice cómo debe reaccionar el sistema bajo prueba a cada uno.", + "description": "Un generador gratuito y de código abierto de archivos de prueba para QA. Produce archivos reales de 30 formatos al tamaño exacto y escribe un manifiesto que dice cómo debe reaccionar el sistema bajo prueba a cada uno.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

Genera archivos de prueba reales al tamaño exacto

- PDF, PNG, DOCX, ZIP - 29 formatos en total, y cada uno es un + PDF, PNG, DOCX, ZIP - 30 formatos en total, y cada uno es un archivo real que se abre en el programa que le corresponde, con exactamente el tamaño que pediste. Cada ejecución también anota qué debe hacer tu aplicación con cada archivo. Línea de comandos y ventana de escritorio, gratis y de código abierto, funcionando por @@ -180,7 +180,7 @@

Genera archivos de prueba reales al tamaño exacto

  • - 29 + 30

    formatos reales, cada uno se abre en el programa que le corresponde

  • @@ -345,7 +345,7 @@

    Pensado para un conjunto de pruebas que corre sin supervisión

    Pide 10485761 bytes y obtén exactamente eso. Un tamaño que un formato no puede alcanzar es un error con un motivo, nunca un archivo del tamaño equivocado.

  • -

    29 formatos reales

    +

    30 formatos reales

    No ceros de relleno con una extensión. Un PNG generado se abre en un visor de imágenes, un DOCX se abre en Word, un ZIP se extrae. Cada uno se comprueba con lectores independientes antes de publicarse.

  • diff --git a/web/public/es/preguntas-frecuentes/index.html b/web/public/es/preguntas-frecuentes/index.html index ec2f33fe..f8c5687d 100644 --- a/web/public/es/preguntas-frecuentes/index.html +++ b/web/public/es/preguntas-frecuentes/index.html @@ -206,7 +206,7 @@

    Preguntas frecuentes

    ¿Qué formatos vienen después?

    -

    7z y mp3. Hoy funcionan de principio a fin 29 formatos.

    +

    7z y mp3. Hoy funcionan de principio a fin 30 formatos.

    @@ -277,7 +277,7 @@

    Preguntas frecuentes

    { "@type": "Question", "name": "¿Qué formatos vienen después?", - "acceptedAnswer": { "@type": "Answer", "text": "7z y mp3. Hoy funcionan de principio a fin 29 formatos." } + "acceptedAnswer": { "@type": "Answer", "text": "7z y mp3. Hoy funcionan de principio a fin 30 formatos." } }, { "@type": "Question", diff --git a/web/public/es/presets/empty-and-minimal/index.html b/web/public/es/presets/empty-and-minimal/index.html index e13377a9..dba23694 100644 --- a/web/public/es/presets/empty-and-minimal/index.html +++ b/web/public/es/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Archivos de prueba válidos mínimos y vacíos en cada formato - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -179,10 +179,10 @@

    ¿Qué hay en el conjunto?

    - - - - + + + +
    Archivos31
    Targets en su receta31
    Tamaño total40 138 B
    Formatosavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
    Archivos32
    Targets en su receta32
    Tamaño total40 427 B
    Formatosavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
    @@ -193,7 +193,7 @@

    ¿Qué hay en el conjunto?

    EsperadoSignificadoArchivos - acceptTu sistema debe aceptar el archivo.29 + acceptTu sistema debe aceptar el archivo.30 unspecifiedDepende de las reglas de tu sistema. Tú decides y después compruebas que lo que ocurre es lo que querías.2 diff --git a/web/public/faq/index.html b/web/public/faq/index.html index a8608e77..6087fa77 100644 --- a/web/public/faq/index.html +++ b/web/public/faq/index.html @@ -205,7 +205,7 @@

    Frequently asked questions

    Which formats are coming next?

    -

    7z and mp3. 29 formats work end to end today.

    +

    7z and mp3. 30 formats work end to end today.

    @@ -276,7 +276,7 @@

    Frequently asked questions

    { "@type": "Question", "name": "Which formats are coming next?", - "acceptedAnswer": { "@type": "Answer", "text": "7z and mp3. 29 formats work end to end today." } + "acceptedAnswer": { "@type": "Answer", "text": "7z and mp3. 30 formats work end to end today." } }, { "@type": "Question", diff --git a/web/public/formats/index.html b/web/public/formats/index.html index a1395f2f..f719fa77 100644 --- a/web/public/formats/index.html +++ b/web/public/formats/index.html @@ -4,8 +4,8 @@ -29 Supported File Formats - PDF, DOCX, PNG, ZIP and More - +30 Supported File Formats - PDF, DOCX, PNG, ZIP and More + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 file formats, every one generated at an exact size

    +

    30 file formats, every one generated at an exact size

    Each of these is a real file of that format. It opens in the software that owns it, and it is exactly the number of bytes you asked for. None of them is padded zeros with an extension @@ -196,6 +196,14 @@

    29 file formats, every one generated at an exact size

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -505,6 +513,46 @@

    Settings each format accepts

    paragraphs 1 - 50000 paragraphs + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + the id of a format, as tfg formats lists them + + + + attachment_size + a size such as 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1007,7 +1055,7 @@

    Settings each format accepts

    Archives hold real files

    - targz and zip + eml and targz and zip can be filled with entries rather than left as an empty shell. A generated archive genuinely contains the documents it claims to contain, so anything that unpacks it during a test finds real files inside. diff --git a/web/public/fr/faq/index.html b/web/public/fr/faq/index.html index c3b978ad..47551aa6 100644 --- a/web/public/fr/faq/index.html +++ b/web/public/fr/faq/index.html @@ -206,7 +206,7 @@

    Questions fréquentes

    Quels formats arrivent ensuite ?

    -

    7z et mp3. 29 formats fonctionnent de bout en bout aujourd'hui.

    +

    7z et mp3. 30 formats fonctionnent de bout en bout aujourd'hui.

    @@ -277,7 +277,7 @@

    Questions fréquentes

    { "@type": "Question", "name": "Quels formats arrivent ensuite ?", - "acceptedAnswer": { "@type": "Answer", "text": "7z et mp3. 29 formats fonctionnent de bout en bout aujourd\u0027hui." } + "acceptedAnswer": { "@type": "Answer", "text": "7z et mp3. 30 formats fonctionnent de bout en bout aujourd\u0027hui." } }, { "@type": "Question", diff --git a/web/public/fr/fichiers-de-test-corrompus/index.html b/web/public/fr/fichiers-de-test-corrompus/index.html index 887ea45f..95c4002d 100644 --- a/web/public/fr/fichiers-de-test-corrompus/index.html +++ b/web/public/fr/fichiers-de-test-corrompus/index.html @@ -202,7 +202,7 @@

    Un fichier abîmé garde la taille demandée

    tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

    Les réglages se placent après deux-points. L'option peut être répétée, et les dommages s'appliquent - dans l'ordre où vous les écrivez. Ça marche avec chacun des 29 formats. + dans l'ordre où vous les écrivez. Ça marche avec chacun des 30 formats.

    diff --git a/web/public/fr/formats/index.html b/web/public/fr/formats/index.html index 4a0f9176..14153be7 100644 --- a/web/public/fr/formats/index.html +++ b/web/public/fr/formats/index.html @@ -4,8 +4,8 @@ -29 formats de fichiers - PDF, DOCX, PNG, ZIP et plus - +30 formats de fichiers - PDF, DOCX, PNG, ZIP et plus + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 formats de fichiers, chacun généré à une taille exacte

    +

    30 formats de fichiers, chacun généré à une taille exacte

    Chacun est un vrai fichier de ce format. Il s'ouvre dans le logiciel qui lui correspond et fait exactement le nombre d'octets que vous avez demandé. Aucun n'est du remplissage @@ -196,6 +196,14 @@

    29 formats de fichiers, chacun généré à une taille exacte

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -507,6 +515,46 @@

    Réglages que chaque format accepte

    paragraphs 1 - 50000 paragraphes + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + l'identifiant d'un format, comme le liste tfg formats + + + + attachment_size + une taille comme 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1010,7 +1058,7 @@

    Réglages que chaque format accepte

    Les archives contiennent de vrais fichiers

    - targz et zip + eml et targz et zip peuvent être remplis d'entrées plutôt que laissés en coquille vide. Une archive générée contient réellement les documents qu'elle prétend contenir, si bien que tout ce qui la décompresse pendant un test y trouve de vrais fichiers. diff --git a/web/public/fr/index.html b/web/public/fr/index.html index 28a5e48f..6bc27da2 100644 --- a/web/public/fr/index.html +++ b/web/public/fr/index.html @@ -4,7 +4,7 @@ -Générateur de fichiers de test - taille exacte, 29 vrais formats +Générateur de fichiers de test - taille exacte, 30 vrais formats @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Un générateur gratuit et open source de fichiers de test pour la QA. Il produit de vrais fichiers dans 29 formats, à la taille exacte, et écrit un manifeste qui dit comment le système testé doit réagir à chacun.", + "description": "Un générateur gratuit et open source de fichiers de test pour la QA. Il produit de vrais fichiers dans 30 formats, à la taille exacte, et écrit un manifeste qui dit comment le système testé doit réagir à chacun.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Générez de vrais fichiers de test à la taille exacte

    - PDF, PNG, DOCX, ZIP - 29 formats au total, et chacun est un + PDF, PNG, DOCX, ZIP - 30 formats au total, et chacun est un vrai fichier qui s'ouvre dans le logiciel qui lui correspond, à la taille exacte que vous avez demandée. Chaque exécution note aussi ce que votre application doit faire de chaque fichier. Ligne de commande et fenêtre de bureau, gratuit et open source, entièrement sur @@ -180,7 +180,7 @@

    Générez de vrais fichiers de test à la taille exacte

    • - 29 + 30

      vrais formats, chacun s'ouvrant dans le logiciel qui lui correspond

    • @@ -345,7 +345,7 @@

      Conçu pour une suite qui tourne sans surveillance

      Demandez 10485761 octets et obtenez exactement cela. Une taille qu'un format ne peut pas atteindre est une erreur avec une raison, jamais un fichier de mauvaise taille.

    • -

      29 vrais formats

      +

      30 vrais formats

      Pas des zéros de remplissage avec une extension. Un PNG généré s'ouvre dans une visionneuse d'images, un DOCX s'ouvre dans Word, un ZIP se décompresse. Chacun est vérifié avec des lecteurs indépendants avant d'être livré.

    • diff --git a/web/public/fr/prereglages/empty-and-minimal/index.html b/web/public/fr/prereglages/empty-and-minimal/index.html index a50c5377..a8008fde 100644 --- a/web/public/fr/prereglages/empty-and-minimal/index.html +++ b/web/public/fr/prereglages/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Fichiers de test valides minimaux et vides, dans chaque format - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -179,10 +179,10 @@

      Que contient le jeu ?

      - - - - + + + +
      Fichiers31
      Cibles dans sa recette31
      Taille totale40 138 B
      Formatsavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Fichiers32
      Cibles dans sa recette32
      Taille totale40 427 B
      Formatsavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -193,7 +193,7 @@

      Que contient le jeu ?

      AttenduSignificationFichiers - acceptVotre système doit accepter le fichier.29 + acceptVotre système doit accepter le fichier.30 unspecifiedCela dépend des règles de votre système. Vous décidez, puis vous vérifiez que ce qui se passe est ce que vous aviez voulu.2 diff --git a/web/public/hi/corrupt-test-files/index.html b/web/public/hi/corrupt-test-files/index.html index 1462b465..6b607e03 100644 --- a/web/public/hi/corrupt-test-files/index.html +++ b/web/public/hi/corrupt-test-files/index.html @@ -200,7 +200,7 @@

      खराब फ़ाइल का आकार वही रहता ह tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      सेटिंग कोलन के बाद लिखी जाती है। विकल्प दोहराया जा सकता है, और बिगाड़ आपके लिखे क्रम में लागू होते - हैं। यह सभी 29 फ़ॉर्मैट के साथ चलता है। + हैं। यह सभी 30 फ़ॉर्मैट के साथ चलता है।

    diff --git a/web/public/hi/faq/index.html b/web/public/hi/faq/index.html index e090f32f..a5b24e01 100644 --- a/web/public/hi/faq/index.html +++ b/web/public/hi/faq/index.html @@ -205,7 +205,7 @@

    अक्सर पूछे जाने वाले सवाल

    आगे कौन से फ़ॉर्मैट आ रहे हैं?

    -

    7z और mp3। आज 29 फ़ॉर्मैट शुरू से अंत तक काम करते हैं।

    +

    7z और mp3। आज 30 फ़ॉर्मैट शुरू से अंत तक काम करते हैं।

    @@ -276,7 +276,7 @@

    अक्सर पूछे जाने वाले सवाल

    { "@type": "Question", "name": "आगे कौन से फ़ॉर्मैट आ रहे हैं?", - "acceptedAnswer": { "@type": "Answer", "text": "7z और mp3। आज 29 फ़ॉर्मैट शुरू से अंत तक काम करते हैं।" } + "acceptedAnswer": { "@type": "Answer", "text": "7z और mp3। आज 30 फ़ॉर्मैट शुरू से अंत तक काम करते हैं।" } }, { "@type": "Question", diff --git a/web/public/hi/formats/index.html b/web/public/hi/formats/index.html index b7d5bb8b..387e5830 100644 --- a/web/public/hi/formats/index.html +++ b/web/public/hi/formats/index.html @@ -4,8 +4,8 @@ -29 समर्थित फ़ाइल फ़ॉर्मैट - PDF, DOCX, PNG, ZIP और अन्य - +30 समर्थित फ़ाइल फ़ॉर्मैट - PDF, DOCX, PNG, ZIP और अन्य + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 फ़ाइल फ़ॉर्मैट, हर एक सटीक आकार में बनाया गया

    +

    30 फ़ाइल फ़ॉर्मैट, हर एक सटीक आकार में बनाया गया

    इनमें से हर एक उस फ़ॉर्मैट की असली फ़ाइल है। वह अपने सॉफ़्टवेयर में खुलती है और उसमें ठीक उतने बाइट हैं जितने आपने माँगे। इनमें से कोई भी एक्सटेंशन चिपकाए गए भराव के शून्य नहीं @@ -196,6 +196,14 @@

    29 फ़ाइल फ़ॉर्मैट, हर एक सटीक आ full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -505,6 +513,46 @@

    हर फ़ॉर्मैट की स्वीकार की जा paragraphs 1 - 50000 अनुच्छेद + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + किसी फ़ॉर्मैट का id, जैसा tfg formats सूचीबद्ध करता है + + + + attachment_size + 2mb जैसा आकार + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1008,7 +1056,7 @@

    हर फ़ॉर्मैट की स्वीकार की जा

    आर्काइव में असली फ़ाइलें होती हैं

    - targz और zip को + eml और targz और zip को खाली खोल छोड़ने के बजाय प्रविष्टियों से भरा जा सकता है। बनाया गया आर्काइव सच में वे दस्तावेज़ रखता है जिनका वह दावा करता है, इसलिए टेस्ट के दौरान उसे खोलने वाली कोई भी चीज़ अंदर असली फ़ाइलें पाती है। diff --git a/web/public/hi/index.html b/web/public/hi/index.html index d584df10..645c0bb1 100644 --- a/web/public/hi/index.html +++ b/web/public/hi/index.html @@ -4,7 +4,7 @@ -QA के लिए टेस्ट फ़ाइल जनरेटर - सटीक आकार, 29 असली फ़ॉर्मैट +QA के लिए टेस्ट फ़ाइल जनरेटर - सटीक आकार, 30 असली फ़ॉर्मैट @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "QA के लिए मुफ़्त और ओपन सोर्स टेस्ट फ़ाइल जनरेटर। यह 29 फ़ॉर्मैट में सटीक आकार की असली फ़ाइलें बनाता है और एक मैनिफ़ेस्ट लिखता है जो बताता है कि जाँचे जा रहे सिस्टम को हर फ़ाइल पर क्या प्रतिक्रिया देनी चाहिए।", + "description": "QA के लिए मुफ़्त और ओपन सोर्स टेस्ट फ़ाइल जनरेटर। यह 30 फ़ॉर्मैट में सटीक आकार की असली फ़ाइलें बनाता है और एक मैनिफ़ेस्ट लिखता है जो बताता है कि जाँचे जा रहे सिस्टम को हर फ़ाइल पर क्या प्रतिक्रिया देनी चाहिए।", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    सटीक आकार की असली टेस्ट फ़ाइलें बनाएँ

    - PDF, PNG, DOCX, ZIP - कुल 29 फ़ॉर्मैट, और हर एक असली फ़ाइल है + PDF, PNG, DOCX, ZIP - कुल 30 फ़ॉर्मैट, और हर एक असली फ़ाइल है जो अपने सॉफ़्टवेयर में खुलती है, ठीक उसी आकार में जो आपने माँगा। हर रन यह भी लिखता है कि आपके एप्लिकेशन को हर फ़ाइल के साथ क्या करना चाहिए। कमांड लाइन और डेस्कटॉप विंडो, मुफ़्त और ओपन सोर्स, पूरी तरह आपकी मशीन पर चलता है। @@ -179,7 +179,7 @@

    सटीक आकार की असली टेस्ट फ़ाइ
    • - 29 + 30

      असली फ़ॉर्मैट, हर एक अपने सॉफ़्टवेयर में खुलता है

    • @@ -341,7 +341,7 @@

      बिना निगरानी के चलने वाले सू

      10485761 बाइट माँगें और ठीक वही पाएँ। जिस आकार तक कोई फ़ॉर्मैट नहीं पहुँच सकता, वह कारण सहित त्रुटि है, गलत आकार की फ़ाइल कभी नहीं।

    • -

      29 असली फ़ॉर्मैट

      +

      30 असली फ़ॉर्मैट

      एक्सटेंशन वाले भराव के शून्य नहीं। बनाई गई PNG इमेज व्यूअर में खुलती है, DOCX Word में खुलता है, ZIP खुल जाता है। हर एक को भेजने से पहले स्वतंत्र रीडरों से जाँचा जाता है।

    • diff --git a/web/public/hi/presets/empty-and-minimal/index.html b/web/public/hi/presets/empty-and-minimal/index.html index 48183cf1..7ad091d8 100644 --- a/web/public/hi/presets/empty-and-minimal/index.html +++ b/web/public/hi/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ हर फ़ॉर्मैट की सबसे छोटी वैध और खाली टेस्ट फ़ाइलें - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

      सेट में क्या है?

      - - - - + + + +
      फ़ाइलें31
      इसकी रेसिपी में टार्गेट31
      कुल आकार40 138 B
      फ़ॉर्मैटavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      फ़ाइलें32
      इसकी रेसिपी में टार्गेट32
      कुल आकार40 427 B
      फ़ॉर्मैटavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -192,7 +192,7 @@

      सेट में क्या है?

      अपेक्षितअर्थफ़ाइलें - acceptआपके सिस्टम को फ़ाइल मंज़ूर करनी चाहिए।29 + acceptआपके सिस्टम को फ़ाइल मंज़ूर करनी चाहिए।30 unspecifiedयह आपके सिस्टम के नियमों पर निर्भर है। आप तय करें, फिर जाँचें कि जो होता है वही है जो आप चाहते थे।2 diff --git a/web/public/id/faq/index.html b/web/public/id/faq/index.html index 8cfb309d..510d8da9 100644 --- a/web/public/id/faq/index.html +++ b/web/public/id/faq/index.html @@ -206,7 +206,7 @@

      Pertanyaan yang sering diajukan

      Format apa yang akan hadir berikutnya?

      -

      7z dan mp3. Saat ini 29 format bekerja dari ujung ke ujung.

      +

      7z dan mp3. Saat ini 30 format bekerja dari ujung ke ujung.

      @@ -277,7 +277,7 @@

      Pertanyaan yang sering diajukan

      { "@type": "Question", "name": "Format apa yang akan hadir berikutnya?", - "acceptedAnswer": { "@type": "Answer", "text": "7z dan mp3. Saat ini 29 format bekerja dari ujung ke ujung." } + "acceptedAnswer": { "@type": "Answer", "text": "7z dan mp3. Saat ini 30 format bekerja dari ujung ke ujung." } }, { "@type": "Question", diff --git a/web/public/id/file-uji-rusak/index.html b/web/public/id/file-uji-rusak/index.html index 21881a89..b0965d06 100644 --- a/web/public/id/file-uji-rusak/index.html +++ b/web/public/id/file-uji-rusak/index.html @@ -201,7 +201,7 @@

      File yang rusak tetap berukuran seperti yang Anda minta

      tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      Pengaturan ditulis setelah titik dua. Opsi ini bisa diulang, dan kerusakan diterapkan menurut urutan - yang Anda tulis. Berlaku untuk semua 29 format. + yang Anda tulis. Berlaku untuk semua 30 format.

    diff --git a/web/public/id/format/index.html b/web/public/id/format/index.html index 2092a6df..cb98ad46 100644 --- a/web/public/id/format/index.html +++ b/web/public/id/format/index.html @@ -4,8 +4,8 @@ -29 Format File yang Didukung - PDF, DOCX, PNG, ZIP, dan Lainnya - +30 Format File yang Didukung - PDF, DOCX, PNG, ZIP, dan Lainnya + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 format file, masing-masing dibuat dengan ukuran tepat

    +

    30 format file, masing-masing dibuat dengan ukuran tepat

    Masing-masing adalah file asli dari format itu. File terbuka di aplikasi yang memilikinya dan persis berjumlah byte yang Anda minta. Tidak ada yang berupa nol pengisi dengan @@ -196,6 +196,14 @@

    29 format file, masing-masing dibuat dengan ukuran tepat

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -505,6 +513,46 @@

    Pengaturan yang diterima setiap format

    paragraphs 1 - 50000 paragraf + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + id sebuah format, seperti yang didaftar tfg formats + + + + attachment_size + ukuran seperti 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1009,7 +1057,7 @@

    Pengaturan yang diterima setiap format

    Arsip berisi file asli

    - targz dan zip + eml dan targz dan zip dapat diisi dengan entri alih-alih dibiarkan sebagai cangkang kosong. Arsip yang dihasilkan benar-benar berisi dokumen yang diklaimnya, sehingga apa pun yang mengekstraknya selama pengujian menemukan file asli di dalamnya. diff --git a/web/public/id/index.html b/web/public/id/index.html index 477163c4..49523b2a 100644 --- a/web/public/id/index.html +++ b/web/public/id/index.html @@ -4,7 +4,7 @@ -Generator File Uji untuk QA - Ukuran Tepat, 29 Format Asli +Generator File Uji untuk QA - Ukuran Tepat, 30 Format Asli @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Generator file uji untuk QA yang gratis dan open source. Alat ini membuat file asli dalam 29 format dengan ukuran tepat dan menulis manifes yang menyatakan bagaimana sistem yang diuji harus bereaksi terhadap tiap file.", + "description": "Generator file uji untuk QA yang gratis dan open source. Alat ini membuat file asli dalam 30 format dengan ukuran tepat dan menulis manifes yang menyatakan bagaimana sistem yang diuji harus bereaksi terhadap tiap file.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Buat file uji asli dengan ukuran tepat

    - PDF, PNG, DOCX, ZIP - 29 format seluruhnya, dan setiap file + PDF, PNG, DOCX, ZIP - 30 format seluruhnya, dan setiap file adalah file asli yang terbuka di aplikasi yang memilikinya, dengan ukuran persis seperti yang Anda minta. Setiap run juga mencatat apa yang harus dilakukan aplikasi Anda terhadap tiap file. Baris perintah dan jendela desktop, gratis dan open source, bekerja @@ -180,7 +180,7 @@

    Buat file uji asli dengan ukuran tepat

    • - 29 + 30

      format asli, masing-masing terbuka di aplikasi yang memilikinya

    • @@ -344,7 +344,7 @@

      Dibuat untuk suite yang berjalan tanpa pengawasan

      Minta 10485761 byte dan dapatkan persis itu. Ukuran yang tidak dapat dicapai format adalah galat dengan alasan, tidak pernah file berukuran salah.

    • -

      29 format asli

      +

      30 format asli

      Bukan nol pengisi dengan ekstensi. PNG yang dihasilkan terbuka di penampil gambar, DOCX terbuka di Word, ZIP bisa diekstrak. Masing-masing diperiksa dengan pembaca independen sebelum dirilis.

    • diff --git a/web/public/id/preset/empty-and-minimal/index.html b/web/public/id/preset/empty-and-minimal/index.html index 4e7adf9f..5898ea5a 100644 --- a/web/public/id/preset/empty-and-minimal/index.html +++ b/web/public/id/preset/empty-and-minimal/index.html @@ -5,7 +5,7 @@ File Uji Valid Terkecil dan Kosong di Setiap Format - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -179,10 +179,10 @@

      Apa isi set?

      - - - - + + + +
      File31
      Target dalam resepnya31
      Ukuran total40 138 B
      Formatavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      File32
      Target dalam resepnya32
      Ukuran total40 427 B
      Formatavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -193,7 +193,7 @@

      Apa isi set?

      DiharapkanArtiFile - acceptSistem Anda harus menerima file ini.29 + acceptSistem Anda harus menerima file ini.30 unspecifiedTergantung pada aturan sistem Anda. Anda yang memutuskan, lalu memeriksa apakah yang terjadi sesuai maksud Anda.2 diff --git a/web/public/index.html b/web/public/index.html index 1b52f100..edca72a4 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -4,7 +4,7 @@ -Test File Generator for QA - Exact Size, 29 Real Formats +Test File Generator for QA - Exact Size, 30 Real Formats @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "A free and open source generator of test files for QA. It produces real files of 29 formats at any exact size and writes a manifest saying how the system under test should react to each one.", + "description": "A free and open source generator of test files for QA. It produces real files of 30 formats at any exact size and writes a manifest saying how the system under test should react to each one.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

      Generate real test files at any exact size

      - PDF, PNG, DOCX, ZIP - 29 formats in all, and every one is a + PDF, PNG, DOCX, ZIP - 30 formats in all, and every one is a real file that opens in the software that owns it, at exactly the size you asked for. Each run also writes down what your application is supposed to do with each file. Command line and desktop window, free and open source, working entirely on your machine. @@ -179,7 +179,7 @@

      Generate real test files at any exact size

      • - 29 + 30

        real formats, each one opening in the software that owns it

      • @@ -342,7 +342,7 @@

        Built for a suite that runs unattended

        Ask for 10485761 bytes and get exactly that. A size a format cannot reach is an error with a reason, never a file of the wrong size.

      • -

        29 real formats

        +

        30 real formats

        Not padded zeros with an extension. A generated PNG opens in an image viewer, a DOCX opens in Word, a ZIP extracts. Each one is checked against independent readers before it ships.

      • diff --git a/web/public/it/domande-frequenti/index.html b/web/public/it/domande-frequenti/index.html index 66937cda..b9ef6621 100644 --- a/web/public/it/domande-frequenti/index.html +++ b/web/public/it/domande-frequenti/index.html @@ -206,7 +206,7 @@

        Domande frequenti

        Quali formati arrivano dopo?

        -

        7z e mp3. Oggi 29 formati funzionano da un capo all'altro.

        +

        7z e mp3. Oggi 30 formati funzionano da un capo all'altro.

        @@ -277,7 +277,7 @@

        Domande frequenti

        { "@type": "Question", "name": "Quali formati arrivano dopo?", - "acceptedAnswer": { "@type": "Answer", "text": "7z e mp3. Oggi 29 formati funzionano da un capo all\u0027altro." } + "acceptedAnswer": { "@type": "Answer", "text": "7z e mp3. Oggi 30 formati funzionano da un capo all\u0027altro." } }, { "@type": "Question", diff --git a/web/public/it/file-di-test-corrotti/index.html b/web/public/it/file-di-test-corrotti/index.html index 780860bc..502f4378 100644 --- a/web/public/it/file-di-test-corrotti/index.html +++ b/web/public/it/file-di-test-corrotti/index.html @@ -201,7 +201,7 @@

        Un file danneggiato ha ancora la dimensione richiesta

        tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

        Le impostazioni vanno dopo i due punti. L'opzione si può ripetere, e i danni vengono applicati - nell'ordine in cui li scrivi. Funziona con ognuno dei 29 formati. + nell'ordine in cui li scrivi. Funziona con ognuno dei 30 formati.

    diff --git a/web/public/it/formati/index.html b/web/public/it/formati/index.html index 94525a71..d117e082 100644 --- a/web/public/it/formati/index.html +++ b/web/public/it/formati/index.html @@ -4,8 +4,8 @@ -29 formati di file supportati - PDF, DOCX, PNG, ZIP e altri - +30 formati di file supportati - PDF, DOCX, PNG, ZIP e altri + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 formati di file, ognuno generato a una dimensione esatta

    +

    30 formati di file, ognuno generato a una dimensione esatta

    Ognuno è un file reale di quel formato. Si apre nel programma che gli appartiene ed è esattamente il numero di byte che hai chiesto. Nessuno è riempimento di zeri con un'estensione @@ -196,6 +196,14 @@

    29 formati di file, ognuno generato a una dimensione esatta

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -507,6 +515,46 @@

    Impostazioni che ogni formato accetta

    paragraphs 1 - 50000 paragrafi + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + l'id di un formato, come lo elenca tfg formats + + + + attachment_size + una dimensione come 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1011,7 +1059,7 @@

    Impostazioni che ogni formato accetta

    Gli archivi contengono file reali

    - targz e zip + eml e targz e zip possono essere riempiti di voci invece di restare un guscio vuoto. Un archivio generato contiene davvero i documenti che dichiara di contenere, quindi qualsiasi cosa lo decomprima durante un test trova file reali al suo interno. diff --git a/web/public/it/index.html b/web/public/it/index.html index 39959dc0..372b0de2 100644 --- a/web/public/it/index.html +++ b/web/public/it/index.html @@ -4,7 +4,7 @@ -Generatore di file di test - dimensione esatta, 29 formati reali +Generatore di file di test - dimensione esatta, 30 formati reali @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Un generatore gratuito e open source di file di test per la QA. Produce file reali in 29 formati della dimensione esatta e scrive un manifest che dice come il sistema sotto test deve reagire a ciascuno.", + "description": "Un generatore gratuito e open source di file di test per la QA. Produce file reali in 30 formati della dimensione esatta e scrive un manifest che dice come il sistema sotto test deve reagire a ciascuno.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Genera file di test reali della dimensione esatta

    - PDF, PNG, DOCX, ZIP - 29 formati in tutto, e ognuno è un file + PDF, PNG, DOCX, ZIP - 30 formati in tutto, e ognuno è un file reale che si apre nel programma che gli appartiene, di esattamente la dimensione che hai chiesto. Ogni esecuzione annota anche cosa la tua applicazione deve fare con ogni file. Riga di comando e finestra desktop, gratuito e open source, interamente sulla tua macchina. @@ -179,7 +179,7 @@

    Genera file di test reali della dimensione esatta

    • - 29 + 30

      formati reali, ognuno si apre nel programma che gli appartiene

    • @@ -344,7 +344,7 @@

      Costruito per una suite che gira senza supervisione

      Chiedi 10485761 byte e ottieni esattamente quelli. Una dimensione che un formato non può raggiungere è un errore con un motivo, mai un file della dimensione sbagliata.

    • -

      29 formati reali

      +

      30 formati reali

      Non zeri di riempimento con un'estensione. Un PNG generato si apre in un visualizzatore di immagini, un DOCX si apre in Word, uno ZIP si estrae. Ognuno è verificato con lettori indipendenti prima del rilascio.

    • diff --git a/web/public/it/preset/empty-and-minimal/index.html b/web/public/it/preset/empty-and-minimal/index.html index 08b9eba7..8b0f47c4 100644 --- a/web/public/it/preset/empty-and-minimal/index.html +++ b/web/public/it/preset/empty-and-minimal/index.html @@ -5,7 +5,7 @@ File di test validi minimi e vuoti in ogni formato - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -179,10 +179,10 @@

      Cosa c'è nel set?

      - - - - + + + +
      File31
      Target nella sua ricetta31
      Dimensione totale40 138 B
      Formatiavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      File32
      Target nella sua ricetta32
      Dimensione totale40 427 B
      Formatiavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -193,7 +193,7 @@

      Cosa c'è nel set?

      AttesoSignificatoFile - acceptIl tuo sistema deve accettare il file.29 + acceptIl tuo sistema deve accettare il file.30 unspecifiedDipende dalle regole del tuo sistema. Decidi tu, poi controlli che ciò che accade sia ciò che intendevi.2 diff --git a/web/public/ja/corrupt-test-files/index.html b/web/public/ja/corrupt-test-files/index.html index 8d70345f..0e5f81f6 100644 --- a/web/public/ja/corrupt-test-files/index.html +++ b/web/public/ja/corrupt-test-files/index.html @@ -189,7 +189,7 @@

      破損したファイルも、求めたサイズのままです

      tfg generate --format png --size 2mb --damage zero-head --out ./out
       tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      - 設定はコロンの後ろに書きます。オプションは繰り返せて、破損は書いた順に適用されます。29種類すべての形式で使えます。 + 設定はコロンの後ろに書きます。オプションは繰り返せて、破損は書いた順に適用されます。30種類すべての形式で使えます。

    diff --git a/web/public/ja/faq/index.html b/web/public/ja/faq/index.html index fa20a071..cbf4b6d7 100644 --- a/web/public/ja/faq/index.html +++ b/web/public/ja/faq/index.html @@ -204,7 +204,7 @@

    よくある質問

    次に対応する形式は何ですか。

    -

    7zとmp3です。現在、29種類の形式がエンドツーエンドで動作します。

    +

    7zとmp3です。現在、30種類の形式がエンドツーエンドで動作します。

    @@ -275,7 +275,7 @@

    よくある質問

    { "@type": "Question", "name": "次に対応する形式は何ですか。", - "acceptedAnswer": { "@type": "Answer", "text": "7zとmp3です。現在、29種類の形式がエンドツーエンドで動作します。" } + "acceptedAnswer": { "@type": "Answer", "text": "7zとmp3です。現在、30種類の形式がエンドツーエンドで動作します。" } }, { "@type": "Question", diff --git a/web/public/ja/formats/index.html b/web/public/ja/formats/index.html index 8d13067d..825de545 100644 --- a/web/public/ja/formats/index.html +++ b/web/public/ja/formats/index.html @@ -4,8 +4,8 @@ -対応する29種類のファイル形式 - PDF、DOCX、PNG、ZIPほか - +対応する30種類のファイル形式 - PDF、DOCX、PNG、ZIPほか + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29種類のファイル形式、どれも正確なサイズで生成

    +

    30種類のファイル形式、どれも正確なサイズで生成

    どれもその形式の本物のファイルです。対応するソフトウェアで開け、要求したバイト数とぴったり一致します。拡張子を貼り付けただけのゼロ埋めはひとつもありません。

    @@ -194,6 +194,14 @@

    29種類のファイル形式、どれも正確なサイズで生成

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -497,6 +505,46 @@

    形式ごとに受け付ける設定

    paragraphs 1 - 50000 段落 + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + tfg formatsが一覧表示する形式のID + + + + attachment_size + 2mbのようなサイズ + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -996,7 +1044,7 @@

    形式ごとに受け付ける設定

    アーカイブには本物のファイルが入ります

    - targzとzipは、空の殻のままではなく、エントリで満たせます。生成されたアーカイブは、含むと言っているドキュメントを実際に含むため、テスト中にそれを展開するものは、中に本物のファイルを見つけます。 + emlとtargzとzipは、空の殻のままではなく、エントリで満たせます。生成されたアーカイブは、含むと言っているドキュメントを実際に含むため、テスト中にそれを展開するものは、中に本物のファイルを見つけます。

    targets:
       - id: bundle
    diff --git a/web/public/ja/index.html b/web/public/ja/index.html
    index 52f526a6..140968ce 100644
    --- a/web/public/ja/index.html
    +++ b/web/public/ja/index.html
    @@ -4,7 +4,7 @@
     
     
     
    -テストファイル生成ツール - 正確なサイズ、29種類の本物の形式
    +テストファイル生成ツール - 正確なサイズ、30種類の本物の形式
     
     
     
    @@ -33,7 +33,7 @@
     
     
     
    -
    +
     
     
     
    @@ -62,7 +62,7 @@
     
     
     
    -
    +
     
     
     
    @@ -76,7 +76,7 @@
       "@type": "SoftwareApplication",
       "name": "Testing Files Generator",
       "alternateName": "tfg",
    -  "description": "QA向けの無料オープンソースのテストファイル生成ツールです。29種類の形式で正確なサイズの本物のファイルを作り、テスト対象のシステムが各ファイルにどう反応すべきかを示すマニフェストを書き出します。",
    +  "description": "QA向けの無料オープンソースのテストファイル生成ツールです。30種類の形式で正確なサイズの本物のファイルを作り、テスト対象のシステムが各ファイルにどう反応すべきかを示すマニフェストを書き出します。",
       "url": "https:\/\/testingfilesgenerator.donislawdev.com/",
       "applicationCategory": "DeveloperApplication",
       "applicationSubCategory": "Software Testing",
    @@ -157,7 +157,7 @@
       

    正確なサイズの本物のテストファイルを生成

    - PDF、PNG、DOCX、ZIPなど、全29種類の形式に対応し、どれも対応するソフトウェアで開ける本物のファイルで、サイズは要求したとおりちょうどです。各実行では、アプリが各ファイルをどう扱うべきかも書き出します。コマンドラインとデスクトップウィンドウの両方に対応し、無料のオープンソースで、すべてあなたのマシン上で動きます。 + PDF、PNG、DOCX、ZIPなど、全30種類の形式に対応し、どれも対応するソフトウェアで開ける本物のファイルで、サイズは要求したとおりちょうどです。各実行では、アプリが各ファイルをどう扱うべきかも書き出します。コマンドラインとデスクトップウィンドウの両方に対応し、無料のオープンソースで、すべてあなたのマシン上で動きます。

    @@ -176,7 +176,7 @@

    正確なサイズの本物のテストファイルを生成

    • - 29 + 30

      種類の本物の形式、どれも対応するソフトウェアで開けます

    • @@ -328,7 +328,7 @@

      無人で動くテストスイートのために作られています

      10485761バイトを要求すれば、ちょうどその大きさになります。形式が到達できないサイズは理由付きのエラーになり、サイズの違うファイルが作られることはありません。

    • -

      29種類の本物の形式

      +

      30種類の本物の形式

      拡張子を付けただけのゼロ埋めではありません。生成したPNGは画像ビューアーで開け、DOCXはWordで開け、ZIPは展開できます。どれも出荷前に独立したリーダーで検証されます。

    • diff --git a/web/public/ja/presets/empty-and-minimal/index.html b/web/public/ja/presets/empty-and-minimal/index.html index a512efcb..2879989f 100644 --- a/web/public/ja/presets/empty-and-minimal/index.html +++ b/web/public/ja/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ 全形式の最小の有効ファイルと空ファイル - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -176,10 +176,10 @@

      セットには何が入っていますか。

      - - - - + + + +
      ファイル数31
      レシピ内のターゲット数31
      合計サイズ40 138 B
      形式avif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      ファイル数32
      レシピ内のターゲット数32
      合計サイズ40 427 B
      形式avif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -190,7 +190,7 @@

      セットには何が入っていますか。

      期待意味ファイル数 - acceptシステムはこのファイルを受け入れるべきです。29 + acceptシステムはこのファイルを受け入れるべきです。30 unspecifiedシステムのルール次第です。あなたが決め、実際に起きることが意図どおりかを確認してください。2 diff --git a/web/public/ko/corrupt-test-files/index.html b/web/public/ko/corrupt-test-files/index.html index a882992b..34e8f9d5 100644 --- a/web/public/ko/corrupt-test-files/index.html +++ b/web/public/ko/corrupt-test-files/index.html @@ -194,7 +194,7 @@

      손상된 파일도 요청한 크기를 유지합니다

      tfg generate --format png --size 2mb --damage zero-head --out ./out
       tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      - 설정은 콜론 뒤에 씁니다. 옵션은 반복할 수 있고, 손상은 쓴 순서대로 적용됩니다. 29개 형식 모두에서 동작합니다. + 설정은 콜론 뒤에 씁니다. 옵션은 반복할 수 있고, 손상은 쓴 순서대로 적용됩니다. 30개 형식 모두에서 동작합니다.

    diff --git a/web/public/ko/faq/index.html b/web/public/ko/faq/index.html index 05e18003..06508ba1 100644 --- a/web/public/ko/faq/index.html +++ b/web/public/ko/faq/index.html @@ -205,7 +205,7 @@

    자주 묻는 질문

    다음에는 어떤 형식이 추가되나요?

    -

    7z와 mp3입니다. 현재 29가지 형식이 처음부터 끝까지 동작합니다.

    +

    7z와 mp3입니다. 현재 30가지 형식이 처음부터 끝까지 동작합니다.

    @@ -276,7 +276,7 @@

    자주 묻는 질문

    { "@type": "Question", "name": "다음에는 어떤 형식이 추가되나요?", - "acceptedAnswer": { "@type": "Answer", "text": "7z와 mp3입니다. 현재 29가지 형식이 처음부터 끝까지 동작합니다." } + "acceptedAnswer": { "@type": "Answer", "text": "7z와 mp3입니다. 현재 30가지 형식이 처음부터 끝까지 동작합니다." } }, { "@type": "Question", diff --git a/web/public/ko/formats/index.html b/web/public/ko/formats/index.html index b523c215..334c73e8 100644 --- a/web/public/ko/formats/index.html +++ b/web/public/ko/formats/index.html @@ -4,8 +4,8 @@ -지원하는 파일 형식 29종 - PDF, DOCX, PNG, ZIP 등 - +지원하는 파일 형식 30종 - PDF, DOCX, PNG, ZIP 등 + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    파일 형식 29가지, 모두 정확한 크기로 생성됩니다

    +

    파일 형식 30가지, 모두 정확한 크기로 생성됩니다

    이들은 모두 해당 형식의 실제 파일입니다. 해당 소프트웨어에서 열리며 요청한 바이트 수와 정확히 같습니다. 확장자만 붙인 채운 0은 하나도 없습니다.

    @@ -194,6 +194,14 @@

    파일 형식 29가지, 모두 정확한 크기로 생성됩니다

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -498,6 +506,46 @@

    각 형식이 받는 설정

    paragraphs 1 - 50000 단락 + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + tfg formats가 나열하는 형식의 id + + + + attachment_size + 2mb와 같은 크기 + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -998,7 +1046,7 @@

    각 형식이 받는 설정

    아카이브에는 실제 파일이 들어 있습니다

    - targz와 zip는 빈 + eml와 targz와 zip는 빈 껍데기로 두지 않고 항목으로 채울 수 있습니다. 생성된 아카이브는 담았다고 하는 문서를 실제로 담고 있으므로, 테스트 중에 이를 푸는 것은 무엇이든 안에서 실제 파일을 찾습니다.

    diff --git a/web/public/ko/index.html b/web/public/ko/index.html index d2b1f230..e4391c7d 100644 --- a/web/public/ko/index.html +++ b/web/public/ko/index.html @@ -4,7 +4,7 @@ -테스트 파일 생성기 - 정확한 크기, 실제 형식 29종 +테스트 파일 생성기 - 정확한 크기, 실제 형식 30종 @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "QA를 위한 무료 오픈 소스 테스트 파일 생성기입니다. 29가지 형식의 정확한 크기의 실제 파일을 만들고, 테스트 대상 시스템이 각 파일에 어떻게 반응해야 하는지 알려 주는 매니페스트를 작성합니다.", + "description": "QA를 위한 무료 오픈 소스 테스트 파일 생성기입니다. 30가지 형식의 정확한 크기의 실제 파일을 만들고, 테스트 대상 시스템이 각 파일에 어떻게 반응해야 하는지 알려 주는 매니페스트를 작성합니다.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    정확한 크기의 실제 테스트 파일을 만드세요

    - PDF, PNG, DOCX, ZIP 등 모두 29가지 형식이며, 모두 해당 소프트웨어에서 열리는 실제 파일로 + PDF, PNG, DOCX, ZIP 등 모두 30가지 형식이며, 모두 해당 소프트웨어에서 열리는 실제 파일로 요청한 크기와 정확히 같습니다. 매 실행마다 애플리케이션이 각 파일을 어떻게 처리해야 하는지도 함께 기록합니다. 명령줄과 데스크톱 창, 무료 오픈 소스이며, 모두 여러분의 컴퓨터에서 동작합니다.

    @@ -178,7 +178,7 @@

    정확한 크기의 실제 테스트 파일을 만드세요

    • - 29 + 30

      가지 실제 형식, 각각 해당 소프트웨어에서 열립니다

    • @@ -335,7 +335,7 @@

      무인으로 실행되는 테스트 묶음을 위해 만들었습니다

      10485761바이트를 요청하면 정확히 그 크기가 나옵니다. 형식이 도달할 수 없는 크기는 이유가 붙은 오류가 되며, 크기가 틀린 파일이 되는 일은 없습니다.

    • -

      실제 형식 29가지

      +

      실제 형식 30가지

      확장자만 붙인 채운 0이 아닙니다. 생성된 PNG는 이미지 뷰어에서 열리고, DOCX는 Word에서 열리며, ZIP은 풀립니다. 모든 형식은 출시 전에 독립적인 리더로 검증됩니다.

    • diff --git a/web/public/ko/presets/empty-and-minimal/index.html b/web/public/ko/presets/empty-and-minimal/index.html index 33ebd247..c5dfa0a8 100644 --- a/web/public/ko/presets/empty-and-minimal/index.html +++ b/web/public/ko/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ 모든 형식의 가장 작은 유효 파일과 빈 파일 - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -177,10 +177,10 @@

      세트에는 무엇이 들어 있나요?

      - - - - + + + +
      파일 수31
      레시피의 타깃 수31
      총 크기40 138 B
      형식avif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      파일 수32
      레시피의 타깃 수32
      총 크기40 427 B
      형식avif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -191,7 +191,7 @@

      세트에는 무엇이 들어 있나요?

      기대값의미파일 수 - accept시스템은 이 파일을 받아야 합니다.29 + accept시스템은 이 파일을 받아야 합니다.30 unspecified시스템의 규칙에 따라 다릅니다. 직접 결정한 뒤 실제 일어나는 일이 의도한 것인지 확인하세요.2 diff --git a/web/public/nl/beschadigde-testbestanden/index.html b/web/public/nl/beschadigde-testbestanden/index.html index 8add9068..a3bf8c7c 100644 --- a/web/public/nl/beschadigde-testbestanden/index.html +++ b/web/public/nl/beschadigde-testbestanden/index.html @@ -203,7 +203,7 @@

      Een beschadigd bestand heeft nog steeds de grootte die je vroeg

      Instellingen komen achter een dubbele punt. De optie mag herhaald worden, en de beschadigingen worden toegepast in de volgorde waarin je ze schrijft. Het werkt met elk van de - 29 formaten. + 30 formaten.

    diff --git a/web/public/nl/formaten/index.html b/web/public/nl/formaten/index.html index d15a4e40..ff7b6443 100644 --- a/web/public/nl/formaten/index.html +++ b/web/public/nl/formaten/index.html @@ -4,8 +4,8 @@ -29 ondersteunde bestandsformaten - PDF, DOCX, PNG, ZIP en meer - +30 ondersteunde bestandsformaten - PDF, DOCX, PNG, ZIP en meer + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 bestandsformaten, elk gegenereerd in een exacte grootte

    +

    30 bestandsformaten, elk gegenereerd in een exacte grootte

    Elk daarvan is een echt bestand van dat formaat. Het opent in het programma waartoe het behoort en is precies het aantal bytes dat je vroeg. Geen enkele is opgevulde nullen met een @@ -196,6 +196,14 @@

    29 bestandsformaten, elk gegenereerd in een exacte grootte

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -507,6 +515,46 @@

    Instellingen die elk formaat accepteert

    paragraphs 1 - 50000 alinea's + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + de id van een formaat, zoals tfg formats ze opsomt + + + + attachment_size + een grootte zoals 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1010,7 +1058,7 @@

    Instellingen die elk formaat accepteert

    Archieven bevatten echte bestanden

    - targz en zip + eml en targz en zip kunnen met items worden gevuld in plaats van als lege huls te blijven. Een gegenereerd archief bevat echt de documenten die het zegt te bevatten, dus alles wat het tijdens een test uitpakt vindt er echte bestanden in. diff --git a/web/public/nl/index.html b/web/public/nl/index.html index f81de301..e5749dd4 100644 --- a/web/public/nl/index.html +++ b/web/public/nl/index.html @@ -4,7 +4,7 @@ -Testbestanden genereren - exacte grootte, 29 echte formaten +Testbestanden genereren - exacte grootte, 30 echte formaten @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Een gratis open source generator van testbestanden voor QA. Hij maakt echte bestanden in 29 formaten in elke exacte grootte en schrijft een manifest dat zegt hoe het geteste systeem op elk bestand moet reageren.", + "description": "Een gratis open source generator van testbestanden voor QA. Hij maakt echte bestanden in 30 formaten in elke exacte grootte en schrijft een manifest dat zegt hoe het geteste systeem op elk bestand moet reageren.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Genereer echte testbestanden in elke exacte grootte

    - PDF, PNG, DOCX, ZIP - 29 formaten in totaal, en elk is een + PDF, PNG, DOCX, ZIP - 30 formaten in totaal, en elk is een echt bestand dat opent in het programma waartoe het behoort, in precies de grootte die je vroeg. Elke run schrijft ook op wat je applicatie met elk bestand moet doen. Opdrachtregel en bureaubladvenster, gratis en open source, volledig op je eigen machine. @@ -179,7 +179,7 @@

    Genereer echte testbestanden in elke exacte grootte

    • - 29 + 30

      echte formaten, elk opent in het programma waartoe het behoort

    • @@ -342,7 +342,7 @@

      Gebouwd voor een suite die onbeheerd draait

      Vraag 10485761 bytes en krijg precies dat. Een grootte die een formaat niet kan halen is een fout met een reden, nooit een bestand van de verkeerde grootte.

    • -

      29 echte formaten

      +

      30 echte formaten

      Geen opgevulde nullen met een extensie. Een gegenereerde PNG opent in een afbeeldingsviewer, een DOCX opent in Word, een ZIP pakt uit. Elk wordt vóór levering gecontroleerd met onafhankelijke lezers.

    • diff --git a/web/public/nl/presets/empty-and-minimal/index.html b/web/public/nl/presets/empty-and-minimal/index.html index a544f729..5a743905 100644 --- a/web/public/nl/presets/empty-and-minimal/index.html +++ b/web/public/nl/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Kleinste geldige en lege testbestanden in elk formaat - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -179,10 +179,10 @@

      Wat zit er in de set?

      - - - - + + + +
      Bestanden31
      Targets in het recept31
      Totale grootte40 138 B
      Formatenavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Bestanden32
      Targets in het recept32
      Totale grootte40 427 B
      Formatenavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -193,7 +193,7 @@

      Wat zit er in de set?

      VerwachtBetekenisBestanden - acceptJe systeem moet het bestand aannemen.29 + acceptJe systeem moet het bestand aannemen.30 unspecifiedHet hangt af van de regels van je systeem. Jij beslist, en controleert daarna of wat er gebeurt is wat je bedoelde.2 diff --git a/web/public/nl/veelgestelde-vragen/index.html b/web/public/nl/veelgestelde-vragen/index.html index 5041deaa..08517d4d 100644 --- a/web/public/nl/veelgestelde-vragen/index.html +++ b/web/public/nl/veelgestelde-vragen/index.html @@ -206,7 +206,7 @@

      Veelgestelde vragen

      Welke formaten komen er nog?

      -

      7z en mp3. Vandaag werken 29 formaten van begin tot eind.

      +

      7z en mp3. Vandaag werken 30 formaten van begin tot eind.

      @@ -277,7 +277,7 @@

      Veelgestelde vragen

      { "@type": "Question", "name": "Welke formaten komen er nog?", - "acceptedAnswer": { "@type": "Answer", "text": "7z en mp3. Vandaag werken 29 formaten van begin tot eind." } + "acceptedAnswer": { "@type": "Answer", "text": "7z en mp3. Vandaag werken 30 formaten van begin tot eind." } }, { "@type": "Question", diff --git a/web/public/pl/faq/index.html b/web/public/pl/faq/index.html index b8e6f72b..df4eba0e 100644 --- a/web/public/pl/faq/index.html +++ b/web/public/pl/faq/index.html @@ -206,7 +206,7 @@

      Najczęstsze pytania

      Jakie formaty są następne w kolejce?

      -

      7z i mp3. Formatów działających dziś od początku do końca jest 29.

      +

      7z i mp3. Formatów działających dziś od początku do końca jest 30.

      @@ -277,7 +277,7 @@

      Najczęstsze pytania

      { "@type": "Question", "name": "Jakie formaty są następne w kolejce?", - "acceptedAnswer": { "@type": "Answer", "text": "7z i mp3. Formatów działających dziś od początku do końca jest 29." } + "acceptedAnswer": { "@type": "Answer", "text": "7z i mp3. Formatów działających dziś od początku do końca jest 30." } }, { "@type": "Question", diff --git a/web/public/pl/formaty/index.html b/web/public/pl/formaty/index.html index 42d570cb..eeb0f988 100644 --- a/web/public/pl/formaty/index.html +++ b/web/public/pl/formaty/index.html @@ -4,7 +4,7 @@ -29 formatów plików testowych - PDF, DOCX, PNG, ZIP +30 formatów plików testowych - PDF, DOCX, PNG, ZIP @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -144,7 +144,7 @@
      -

      29 formatów plików, każdy generowany o dokładnym rozmiarze

      +

      30 formatów plików, każdy generowany o dokładnym rozmiarze

      Każdy z nich to prawdziwy plik tego formatu. Otwiera się w programie, do którego należy, i ma dokładnie tyle bajtów, ile zamówiłeś. Żaden nie jest zerami z doklejonym rozszerzeniem. @@ -195,6 +195,14 @@

      29 formatów plików, każdy generowany o dokładnym rozmiarze

      full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -505,6 +513,46 @@

      Ustawienia, które przyjmuje każdy format

      paragraphs 1 - 50000 akapitów + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + identyfikator formatu, tak jak wypisuje go tfg formats + + + + attachment_size + rozmiar, na przykład 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1008,7 +1056,7 @@

      Ustawienia, które przyjmuje każdy format

      Archiwa zawierają prawdziwe pliki

      - targz oraz zip + eml oraz targz oraz zip można wypełnić wpisami, zamiast zostawiać jako pustą skorupę. Wygenerowane archiwum naprawdę zawiera dokumenty, o których mówi, więc cokolwiek rozpakowuje je w trakcie testu, znajduje w środku prawdziwe pliki. diff --git a/web/public/pl/index.html b/web/public/pl/index.html index a3643eb0..8ee9f496 100644 --- a/web/public/pl/index.html +++ b/web/public/pl/index.html @@ -4,7 +4,7 @@ -Generator plików testowych o zadanym rozmiarze - 29 formatów +Generator plików testowych o zadanym rozmiarze - 30 formatów @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Darmowy generator plików testowych dla QA o otwartym kodzie. Tworzy prawdziwe pliki w 29 formatach o dokładnie zadanym rozmiarze i zapisuje manifest mówiący, jak testowany system ma na każdy z nich zareagować.", + "description": "Darmowy generator plików testowych dla QA o otwartym kodzie. Tworzy prawdziwe pliki w 30 formatach o dokładnie zadanym rozmiarze i zapisuje manifest mówiący, jak testowany system ma na każdy z nich zareagować.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

      Generuj pliki testowe o zadanym rozmiarze

      - PDF, PNG, DOCX, ZIP - razem 29 formatów, a każdy to + PDF, PNG, DOCX, ZIP - razem 30 formatów, a każdy to prawdziwy plik, który otwiera się w programie, do którego należy, i ma dokładnie taki rozmiar, o jaki poprosisz. Każdy przebieg zapisuje też, co Twoja aplikacja ma z każdym plikiem zrobić. Wiersz poleceń i okno, darmowe i otwarte, działające wyłącznie na Twojej maszynie. @@ -179,7 +179,7 @@

      Generuj pliki testowe o zadanym rozmiarze

      • - 29 + 30

        prawdziwych formatów, każdy otwiera się w programie, do którego należy

      • @@ -342,7 +342,7 @@

        Zbudowane pod zestaw, który chodzi bez nadzoru

        Poproś o 10485761 bajtów i tyle dostaniesz. Rozmiar nieosiągalny dla formatu to błąd z powodem, nigdy plik o innym rozmiarze.

      • -

        29 prawdziwych formatów

        +

        30 prawdziwych formatów

        Nie zera z doklejonym rozszerzeniem. Wygenerowany PNG otwiera się w przeglądarce obrazów, DOCX w Wordzie, a ZIP się rozpakowuje. Każdy format jest sprawdzany niezależnym czytnikiem, zanim trafi do wydania.

      • diff --git a/web/public/pl/presety/empty-and-minimal/index.html b/web/public/pl/presety/empty-and-minimal/index.html index a79fd946..c41647c9 100644 --- a/web/public/pl/presety/empty-and-minimal/index.html +++ b/web/public/pl/presety/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Najmniejsze poprawne i puste pliki w każdym formacie - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

        Co jest w zestawie?

        - - - - + + + +
        Pliki31
        Cele w przepisie31
        Łączny rozmiar40 138 B
        Formatyavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
        Pliki32
        Cele w przepisie32
        Łączny rozmiar40 427 B
        Formatyavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
        @@ -192,7 +192,7 @@

        Co jest w zestawie?

        OczekiwanieZnaczeniePliki - acceptSystem powinien przyjąć plik.29 + acceptSystem powinien przyjąć plik.30 unspecifiedZależy od reguł Twojego systemu. Ty decydujesz, a potem sprawdzasz, czy dzieje się to, co zamierzałeś.2 diff --git a/web/public/pl/uszkodzone-pliki-testowe/index.html b/web/public/pl/uszkodzone-pliki-testowe/index.html index 22658b43..08162fa7 100644 --- a/web/public/pl/uszkodzone-pliki-testowe/index.html +++ b/web/public/pl/uszkodzone-pliki-testowe/index.html @@ -202,7 +202,7 @@

        Uszkodzony plik ma nadal rozmiar, o który prosiłeś

        tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

        Ustawienia podaje się po dwukropku. Flagę można powtarzać, a uszkodzenia są stosowane w kolejności, - w jakiej je zapiszesz. Działa z każdym z 29 formatów. + w jakiej je zapiszesz. Działa z każdym z 30 formatów.

      diff --git a/web/public/presets/empty-and-minimal/index.html b/web/public/presets/empty-and-minimal/index.html index b1d79514..1d3a2cec 100644 --- a/web/public/presets/empty-and-minimal/index.html +++ b/web/public/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Smallest Valid and Empty Test Files in Every Format - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

      What is in the set?

      - - - - + + + +
      Files31
      Targets in its recipe31
      Total size40 138 B
      Formatsavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Files32
      Targets in its recipe32
      Total size40 427 B
      Formatsavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -192,7 +192,7 @@

      What is in the set?

      ExpectedMeaningFiles - acceptYour system should take the file.29 + acceptYour system should take the file.30 unspecifiedIt depends on the rules of your system. You decide, then check that what happens is what you meant.2 diff --git a/web/public/pt-br/arquivos-de-teste-corrompidos/index.html b/web/public/pt-br/arquivos-de-teste-corrompidos/index.html index 32d9fdd5..b953cfae 100644 --- a/web/public/pt-br/arquivos-de-teste-corrompidos/index.html +++ b/web/public/pt-br/arquivos-de-teste-corrompidos/index.html @@ -201,7 +201,7 @@

      Um arquivo danificado continua com o tamanho que você pediu

      tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      As configurações vão depois de dois-pontos. A opção pode ser repetida, e os danos são aplicados na - ordem em que você os escreve. Funciona com cada um dos 29 formatos. + ordem em que você os escreve. Funciona com cada um dos 30 formatos.

    diff --git a/web/public/pt-br/formatos/index.html b/web/public/pt-br/formatos/index.html index c4c63456..194b73d1 100644 --- a/web/public/pt-br/formatos/index.html +++ b/web/public/pt-br/formatos/index.html @@ -4,8 +4,8 @@ -29 formatos de arquivo - PDF, DOCX, PNG, ZIP e mais - +30 formatos de arquivo - PDF, DOCX, PNG, ZIP e mais + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 formatos de arquivo, cada um gerado em um tamanho exato

    +

    30 formatos de arquivo, cada um gerado em um tamanho exato

    Cada um é um arquivo real daquele formato. Abre no programa a que pertence e tem exatamente o número de bytes que você pediu. Nenhum é preenchimento de zeros com uma extensão @@ -196,6 +196,14 @@

    29 formatos de arquivo, cada um gerado em um tamanho exato

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -506,6 +514,46 @@

    Configurações que cada formato aceita

    paragraphs 1 - 50000 parágrafos + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + o id de um formato, como o tfg formats lista + + + + attachment_size + um tamanho como 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1009,7 +1057,7 @@

    Configurações que cada formato aceita

    Arquivos compactados contêm arquivos reais

    - targz e zip + eml e targz e zip podem ser preenchidos com entradas em vez de ficarem como uma casca vazia. Um arquivo compactado gerado realmente contém os documentos que diz conter, então qualquer coisa que o descompacte durante um teste encontra arquivos reais dentro. diff --git a/web/public/pt-br/index.html b/web/public/pt-br/index.html index f80813fa..3581709b 100644 --- a/web/public/pt-br/index.html +++ b/web/public/pt-br/index.html @@ -4,7 +4,7 @@ -Gerador de arquivos de teste - tamanho exato, 29 formatos reais +Gerador de arquivos de teste - tamanho exato, 30 formatos reais @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Um gerador gratuito e de código aberto de arquivos de teste para QA. Produz arquivos reais de 29 formatos no tamanho exato e escreve um manifesto que diz como o sistema sob teste deve reagir a cada um.", + "description": "Um gerador gratuito e de código aberto de arquivos de teste para QA. Produz arquivos reais de 30 formatos no tamanho exato e escreve um manifesto que diz como o sistema sob teste deve reagir a cada um.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Gere arquivos de teste reais no tamanho exato

    - PDF, PNG, DOCX, ZIP - 29 formatos no total, e cada um é um + PDF, PNG, DOCX, ZIP - 30 formatos no total, e cada um é um arquivo real que abre no programa a que pertence, com exatamente o tamanho que você pediu. Cada execução também registra o que sua aplicação deve fazer com cada arquivo. Linha de comando e janela de desktop, gratuito e de código aberto, funcionando inteiramente na @@ -180,7 +180,7 @@

    Gere arquivos de teste reais no tamanho exato

    • - 29 + 30

      formatos reais, cada um abre no programa a que pertence

    • @@ -345,7 +345,7 @@

      Feito para uma suíte que roda sem supervisão

      Peça 10485761 bytes e receba exatamente isso. Um tamanho que um formato não alcança é um erro com um motivo, nunca um arquivo de tamanho errado.

    • -

      29 formatos reais

      +

      30 formatos reais

      Não são zeros de preenchimento com uma extensão. Um PNG gerado abre em um visualizador de imagens, um DOCX abre no Word, um ZIP extrai. Cada um é verificado com leitores independentes antes de ser liberado.

    • diff --git a/web/public/pt-br/perguntas-frequentes/index.html b/web/public/pt-br/perguntas-frequentes/index.html index ef40134a..eafe4e05 100644 --- a/web/public/pt-br/perguntas-frequentes/index.html +++ b/web/public/pt-br/perguntas-frequentes/index.html @@ -206,7 +206,7 @@

      Perguntas frequentes

      Quais formatos vêm a seguir?

      -

      7z e mp3. Hoje 29 formatos funcionam de ponta a ponta.

      +

      7z e mp3. Hoje 30 formatos funcionam de ponta a ponta.

      @@ -277,7 +277,7 @@

      Perguntas frequentes

      { "@type": "Question", "name": "Quais formatos vêm a seguir?", - "acceptedAnswer": { "@type": "Answer", "text": "7z e mp3. Hoje 29 formatos funcionam de ponta a ponta." } + "acceptedAnswer": { "@type": "Answer", "text": "7z e mp3. Hoje 30 formatos funcionam de ponta a ponta." } }, { "@type": "Question", diff --git a/web/public/pt-br/presets/empty-and-minimal/index.html b/web/public/pt-br/presets/empty-and-minimal/index.html index f42a8527..b89325ff 100644 --- a/web/public/pt-br/presets/empty-and-minimal/index.html +++ b/web/public/pt-br/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Menores arquivos de teste válidos e vazios em cada formato - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

      O que há no conjunto?

      - - - - + + + +
      Arquivos31
      Targets na sua receita31
      Tamanho total40 138 B
      Formatosavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Arquivos32
      Targets na sua receita32
      Tamanho total40 427 B
      Formatosavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -192,7 +192,7 @@

      O que há no conjunto?

      EsperadoSignificadoArquivos - acceptSeu sistema deve aceitar o arquivo.29 + acceptSeu sistema deve aceitar o arquivo.30 unspecifiedDepende das regras do seu sistema. Você decide e depois confere se o que acontece é o que você queria.2 diff --git a/web/public/ro/fisiere-de-test-corupte/index.html b/web/public/ro/fisiere-de-test-corupte/index.html index ba1c5f4e..6ca8992c 100644 --- a/web/public/ro/fisiere-de-test-corupte/index.html +++ b/web/public/ro/fisiere-de-test-corupte/index.html @@ -202,7 +202,7 @@

      Un fișier deteriorat are în continuare mărimea cerută

      tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      Setările se scriu după două puncte. Opțiunea poate fi repetată, iar deteriorările se aplică în - ordinea în care le scrii. Funcționează cu fiecare dintre cele 29 formate. + ordinea în care le scrii. Funcționează cu fiecare dintre cele 30 formate.

    diff --git a/web/public/ro/formate/index.html b/web/public/ro/formate/index.html index 0a39e8a5..73ff3420 100644 --- a/web/public/ro/formate/index.html +++ b/web/public/ro/formate/index.html @@ -4,8 +4,8 @@ -29 formate de fișiere - PDF, DOCX, PNG, ZIP și altele - +30 formate de fișiere - PDF, DOCX, PNG, ZIP și altele + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 formate de fișiere, fiecare generat la o dimensiune exactă

    +

    30 formate de fișiere, fiecare generat la o dimensiune exactă

    Fiecare este un fișier real al acelui format. Se deschide în programul căruia îi aparține și are exact numărul de octeți pe care l-ai cerut. Niciunul nu e umplutură de zerouri cu @@ -196,6 +196,14 @@

    29 formate de fișiere, fiecare generat la o dimensiune exactă

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -506,6 +514,46 @@

    Setările pe care le acceptă fiecare format

    paragraphs 1 - 50000 paragrafe + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + id-ul unui format, așa cum îl listează tfg formats + + + + attachment_size + o dimensiune precum 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1009,7 +1057,7 @@

    Setările pe care le acceptă fiecare format

    Arhivele conțin fișiere reale

    - targz și zip pot + eml și targz și zip pot fi umplute cu intrări, nu lăsate ca o carcasă goală. O arhivă generată conține cu adevărat documentele pe care spune că le conține, așa că tot ce o dezarhivează în timpul unui test găsește înăuntru fișiere reale. diff --git a/web/public/ro/index.html b/web/public/ro/index.html index ff79817e..76399855 100644 --- a/web/public/ro/index.html +++ b/web/public/ro/index.html @@ -4,7 +4,7 @@ -Generator de fișiere de test - dimensiune exactă, 29 formate reale +Generator de fișiere de test - dimensiune exactă, 30 formate reale @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Un generator gratuit și open source de fișiere de test pentru QA. Produce fișiere reale în 29 formate, la dimensiunea exactă, și scrie un manifest care spune cum trebuie să reacționeze sistemul testat la fiecare.", + "description": "Un generator gratuit și open source de fișiere de test pentru QA. Produce fișiere reale în 30 formate, la dimensiunea exactă, și scrie un manifest care spune cum trebuie să reacționeze sistemul testat la fiecare.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Generează fișiere de test reale la dimensiunea exactă

    - PDF, PNG, DOCX, ZIP - 29 formate în total, și fiecare este un + PDF, PNG, DOCX, ZIP - 30 formate în total, și fiecare este un fișier real care se deschide în programul căruia îi aparține, la exact dimensiunea pe care ai cerut-o. Fiecare rulare notează și ce trebuie să facă aplicația ta cu fiecare fișier. Linie de comandă și fereastră desktop, gratuit și open source, funcționând în întregime @@ -180,7 +180,7 @@

    Generează fișiere de test reale la dimensiunea exactă

    • - 29 + 30

      formate reale, fiecare se deschide în programul căruia îi aparține

    • @@ -343,7 +343,7 @@

      Construit pentru o suită care rulează nesupravegheată

      Cere 10485761 de octeți și primești exact atât. O dimensiune pe care un format nu o poate atinge este o eroare cu un motiv, niciodată un fișier de dimensiune greșită.

    • -

      29 formate reale

      +

      30 formate reale

      Nu zerouri de umplutură cu o extensie. Un PNG generat se deschide într-un vizualizator de imagini, un DOCX se deschide în Word, un ZIP se extrage. Fiecare este verificat cu cititori independenți înainte de livrare.

    • diff --git a/web/public/ro/intrebari-frecvente/index.html b/web/public/ro/intrebari-frecvente/index.html index 53a85d84..6c4808fe 100644 --- a/web/public/ro/intrebari-frecvente/index.html +++ b/web/public/ro/intrebari-frecvente/index.html @@ -206,7 +206,7 @@

      Întrebări frecvente

      Ce formate urmează?

      -

      7z și mp3. Azi funcționează de la un capăt la altul 29 formate.

      +

      7z și mp3. Azi funcționează de la un capăt la altul 30 formate.

      @@ -277,7 +277,7 @@

      Întrebări frecvente

      { "@type": "Question", "name": "Ce formate urmează?", - "acceptedAnswer": { "@type": "Answer", "text": "7z și mp3. Azi funcționează de la un capăt la altul 29 formate." } + "acceptedAnswer": { "@type": "Answer", "text": "7z și mp3. Azi funcționează de la un capăt la altul 30 formate." } }, { "@type": "Question", diff --git a/web/public/ro/presetari/empty-and-minimal/index.html b/web/public/ro/presetari/empty-and-minimal/index.html index bfd0a851..2de6abf5 100644 --- a/web/public/ro/presetari/empty-and-minimal/index.html +++ b/web/public/ro/presetari/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Cele mai mici fișiere de test valide și goale, în fiecare format - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -179,10 +179,10 @@

      Ce este în set?

      - - - - + + + +
      Fișiere31
      Targeturi în rețeta lui31
      Dimensiune totală40 138 B
      Formateavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Fișiere32
      Targeturi în rețeta lui32
      Dimensiune totală40 427 B
      Formateavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -193,7 +193,7 @@

      Ce este în set?

      AșteptatSemnificațieFișiere - acceptSistemul tău trebuie să accepte fișierul.29 + acceptSistemul tău trebuie să accepte fișierul.30 unspecifiedDepinde de regulile sistemului tău. Tu decizi, apoi verifici că ce se întâmplă este ce ai vrut.2 diff --git a/web/public/ru/corrupt-test-files/index.html b/web/public/ru/corrupt-test-files/index.html index 1702281e..23fe28ce 100644 --- a/web/public/ru/corrupt-test-files/index.html +++ b/web/public/ru/corrupt-test-files/index.html @@ -202,7 +202,7 @@

      Повреждённый файл остаётся нужного разме tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      Настройки пишутся после двоеточия. Параметр можно повторять, а повреждения применяются в том - порядке, в каком вы их записали. Это работает с каждым из 29 форматов. + порядке, в каком вы их записали. Это работает с каждым из 30 форматов.

    diff --git a/web/public/ru/faq/index.html b/web/public/ru/faq/index.html index 4be01528..686300fc 100644 --- a/web/public/ru/faq/index.html +++ b/web/public/ru/faq/index.html @@ -206,7 +206,7 @@

    Часто задаваемые вопросы

    Какие форматы появятся дальше?

    -

    7z и mp3. Сегодня полностью работают 29 форматов.

    +

    7z и mp3. Сегодня полностью работают 30 форматов.

    @@ -277,7 +277,7 @@

    Часто задаваемые вопросы

    { "@type": "Question", "name": "Какие форматы появятся дальше?", - "acceptedAnswer": { "@type": "Answer", "text": "7z и mp3. Сегодня полностью работают 29 форматов." } + "acceptedAnswer": { "@type": "Answer", "text": "7z и mp3. Сегодня полностью работают 30 форматов." } }, { "@type": "Question", diff --git a/web/public/ru/formats/index.html b/web/public/ru/formats/index.html index 9a3ab22a..0c3b5d31 100644 --- a/web/public/ru/formats/index.html +++ b/web/public/ru/formats/index.html @@ -4,8 +4,8 @@ -29 форматов файлов - PDF, DOCX, PNG, ZIP и другие - +30 форматов файлов - PDF, DOCX, PNG, ZIP и другие + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 форматов файлов, каждый создаётся точного размера

    +

    30 форматов файлов, каждый создаётся точного размера

    Каждый из них - настоящий файл этого формата. Он открывается в своей программе и имеет ровно то число байт, которое вы запросили. Ни один не является нулями-заполнителями с @@ -196,6 +196,14 @@

    29 форматов файлов, каждый создаётся точно full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -505,6 +513,46 @@

    Настройки, которые принимает каждый форм paragraphs 1 - 50000 абзацев + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + id формата в том виде, как его выводит tfg formats + + + + attachment_size + размер, например 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1008,7 +1056,7 @@

    Настройки, которые принимает каждый форм

    Архивы содержат настоящие файлы

    - targz и zip + eml и targz и zip можно наполнить записями, а не оставлять пустой оболочкой. Созданный архив действительно содержит документы, которые заявляет, поэтому всё, что распаковывает его во время теста, находит внутри настоящие файлы. diff --git a/web/public/ru/index.html b/web/public/ru/index.html index 990453a5..30a67d5d 100644 --- a/web/public/ru/index.html +++ b/web/public/ru/index.html @@ -4,7 +4,7 @@ -Генератор тестовых файлов для QA - точный размер, 29 форматов +Генератор тестовых файлов для QA - точный размер, 30 форматов @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Бесплатный генератор тестовых файлов для QA с открытым кодом. Создаёт настоящие файлы в 29 форматах точного размера и записывает манифест с ожидаемой реакцией тестируемой системы на каждый из них.", + "description": "Бесплатный генератор тестовых файлов для QA с открытым кодом. Создаёт настоящие файлы в 30 форматах точного размера и записывает манифест с ожидаемой реакцией тестируемой системы на каждый из них.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Создавайте настоящие тестовые файлы точного размера

    - PDF, PNG, DOCX, ZIP - всего 29 форматов, и каждый из них + PDF, PNG, DOCX, ZIP - всего 30 форматов, и каждый из них настоящий файл, который открывается в своей программе, точно того размера, который вы запросили. Каждый запуск ещё и записывает, что ваше приложение должно делать с каждым файлом. Командная строка и десктопное окно, бесплатно и с открытым кодом, всё работает на вашей @@ -180,7 +180,7 @@

    Создавайте настоящие тестовые файлы точн
    • - 29 + 30

      настоящих форматов, каждый открывается в своей программе

    • @@ -343,7 +343,7 @@

      Создан для набора тестов, работающего без

      Запросите 10485761 байт и получите ровно столько. Размер, которого формат не может достичь, - это ошибка с причиной, а не файл неверного размера.

    • -

      29 настоящих форматов

      +

      30 настоящих форматов

      Не нули-заполнители с расширением. Созданный PNG открывается в просмотрщике изображений, DOCX - в Word, ZIP распаковывается. Каждый формат проверяется независимыми читателями до выпуска.

    • diff --git a/web/public/ru/presets/empty-and-minimal/index.html b/web/public/ru/presets/empty-and-minimal/index.html index 5b2b8454..b623f36d 100644 --- a/web/public/ru/presets/empty-and-minimal/index.html +++ b/web/public/ru/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Минимальные корректные и пустые тестовые файлы в каждом формате - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

      Что входит в набор?

      - - - - + + + +
      Файлов31
      Целей в его рецепте31
      Общий размер40 138 B
      Форматыavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Файлов32
      Целей в его рецепте32
      Общий размер40 427 B
      Форматыavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -192,7 +192,7 @@

      Что входит в набор?

      ОжидаетсяЗначениеФайлов - acceptВаша система должна принять файл.29 + acceptВаша система должна принять файл.30 unspecifiedЗависит от правил вашей системы. Вы решаете, а затем проверяете, что происходит именно то, что вы имели в виду.2 diff --git a/web/public/social.html b/web/public/social.html index 6d88976c..a81dd581 100644 --- a/web/public/social.html +++ b/web/public/social.html @@ -203,7 +203,7 @@

      Test sets,
      not just files.

      • Presets - ready test sets for size limits, file names, encodings and imports
      • Recipes - one YAML file, same bytes in every CI run
      • -
      • 29 formats with their own settings - pages, pixels, rows, archive contents
      • +
      • 30 formats with their own settings - pages, pixels, rows, archive contents

      Free and open source · works offline · Windows · macOS · Linux

    diff --git a/web/public/th/corrupt-test-files/index.html b/web/public/th/corrupt-test-files/index.html index a21d6d96..77bc32c0 100644 --- a/web/public/th/corrupt-test-files/index.html +++ b/web/public/th/corrupt-test-files/index.html @@ -199,7 +199,7 @@

    ไฟล์ที่เสียหายยังมีขนาดตา tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

    การตั้งค่าเขียนหลังเครื่องหมายทวิภาค ตัวเลือกนี้ใส่ซ้ำได้ - และความเสียหายจะถูกนำมาใช้ตามลำดับที่คุณเขียน ใช้ได้กับทั้ง 29 รูปแบบ + และความเสียหายจะถูกนำมาใช้ตามลำดับที่คุณเขียน ใช้ได้กับทั้ง 30 รูปแบบ

    diff --git a/web/public/th/faq/index.html b/web/public/th/faq/index.html index a27f70ee..864bedc0 100644 --- a/web/public/th/faq/index.html +++ b/web/public/th/faq/index.html @@ -205,7 +205,7 @@

    คำถามที่พบบ่อย

    รูปแบบใดจะมาต่อไป

    -

    7z และ mp3 ตอนนี้ 29 รูปแบบทำงานได้ครบตั้งแต่ต้นจนจบ

    +

    7z และ mp3 ตอนนี้ 30 รูปแบบทำงานได้ครบตั้งแต่ต้นจนจบ

    @@ -276,7 +276,7 @@

    คำถามที่พบบ่อย

    { "@type": "Question", "name": "รูปแบบใดจะมาต่อไป", - "acceptedAnswer": { "@type": "Answer", "text": "7z และ mp3 ตอนนี้ 29 รูปแบบทำงานได้ครบตั้งแต่ต้นจนจบ" } + "acceptedAnswer": { "@type": "Answer", "text": "7z และ mp3 ตอนนี้ 30 รูปแบบทำงานได้ครบตั้งแต่ต้นจนจบ" } }, { "@type": "Question", diff --git a/web/public/th/formats/index.html b/web/public/th/formats/index.html index 6ca32bfc..16766313 100644 --- a/web/public/th/formats/index.html +++ b/web/public/th/formats/index.html @@ -4,8 +4,8 @@ -29 รูปแบบไฟล์ที่รองรับ - PDF, DOCX, PNG, ZIP และอื่นๆ - +30 รูปแบบไฟล์ที่รองรับ - PDF, DOCX, PNG, ZIP และอื่นๆ + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 รูปแบบไฟล์ แต่ละรูปแบบสร้างที่ขนาดแม่นยำ

    +

    30 รูปแบบไฟล์ แต่ละรูปแบบสร้างที่ขนาดแม่นยำ

    ทุกรูปแบบเป็นไฟล์จริงของรูปแบบนั้น เปิดได้ในซอฟต์แวร์ของมัน และมีจำนวนไบต์ตรงตามที่คุณขอพอดี ไม่มีรูปแบบใดเป็นศูนย์เติมเต็มที่แปะนามสกุลเข้าไป @@ -195,6 +195,14 @@

    29 รูปแบบไฟล์ แต่ละรูปแบบสร้ full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -503,6 +511,46 @@

    การตั้งค่าที่แต่ละรูปแบบร paragraphs 1 - 50000 ย่อหน้า + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + รหัสของรูปแบบ ตามที่ tfg formats แสดง + + + + attachment_size + ขนาด เช่น 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1005,7 +1053,7 @@

    การตั้งค่าที่แต่ละรูปแบบร

    ไฟล์บีบอัดมีไฟล์จริงอยู่ข้างใน

    - targz และ zip + eml และ targz และ zip ใส่รายการลงไปได้ แทนที่จะปล่อยเป็นเปลือกว่าง ไฟล์บีบอัดที่สร้างขึ้นมีเอกสารที่อ้างว่ามีจริงๆ ดังนั้นสิ่งใดที่แตกไฟล์นั้นระหว่างการทดสอบจะพบไฟล์จริงอยู่ข้างใน

    diff --git a/web/public/th/index.html b/web/public/th/index.html index 06f4c585..b68fa5c7 100644 --- a/web/public/th/index.html +++ b/web/public/th/index.html @@ -4,7 +4,7 @@ -ตัวสร้างไฟล์ทดสอบสำหรับ QA - ขนาดแม่นยำ 29 รูปแบบจริง +ตัวสร้างไฟล์ทดสอบสำหรับ QA - ขนาดแม่นยำ 30 รูปแบบจริง @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "ตัวสร้างไฟล์ทดสอบสำหรับ QA ฟรีและโอเพนซอร์ส สร้างไฟล์จริงใน 29 รูปแบบด้วยขนาดแม่นยำ และเขียนแมนิเฟสต์ที่บอกว่าระบบที่ถูกทดสอบควรตอบสนองต่อแต่ละไฟล์อย่างไร", + "description": "ตัวสร้างไฟล์ทดสอบสำหรับ QA ฟรีและโอเพนซอร์ส สร้างไฟล์จริงใน 30 รูปแบบด้วยขนาดแม่นยำ และเขียนแมนิเฟสต์ที่บอกว่าระบบที่ถูกทดสอบควรตอบสนองต่อแต่ละไฟล์อย่างไร", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    สร้างไฟล์ทดสอบจริงที่ขนาดแม่นยำ

    - PDF, PNG, DOCX, ZIP - รวม 29 รูปแบบ + PDF, PNG, DOCX, ZIP - รวม 30 รูปแบบ และทุกไฟล์เป็นไฟล์จริงที่เปิดได้ในซอฟต์แวร์ของมัน ด้วยขนาดตรงตามที่คุณขอพอดี ทุกครั้งที่รันยังจดด้วยว่าแอปพลิเคชันของคุณควรทำอย่างไรกับแต่ละไฟล์ มีทั้งบรรทัดคำสั่งและหน้าต่างเดสก์ท็อป ฟรีและโอเพนซอร์ส ทำงานทั้งหมดบนเครื่องของคุณ @@ -179,7 +179,7 @@

    สร้างไฟล์ทดสอบจริงที่ขนาด
    • - 29 + 30

      รูปแบบจริง แต่ละรูปแบบเปิดได้ในซอฟต์แวร์ของมัน

    • @@ -341,7 +341,7 @@

      สร้างมาสำหรับชุดทดสอบที่ร

      ขอ 10485761 ไบต์ แล้วได้ตามนั้นพอดี ขนาดที่รูปแบบไปไม่ถึงคือข้อผิดพลาดที่มีเหตุผล ไม่ใช่ไฟล์ขนาดผิด

    • -

      29 รูปแบบจริง

      +

      30 รูปแบบจริง

      ไม่ใช่ศูนย์ที่เติมเต็มพร้อมนามสกุล PNG ที่สร้างเปิดได้ในโปรแกรมดูภาพ DOCX เปิดได้ใน Word และ ZIP แตกไฟล์ได้ ทุกรูปแบบถูกตรวจด้วยตัวอ่านอิสระก่อนปล่อย

    • diff --git a/web/public/th/presets/empty-and-minimal/index.html b/web/public/th/presets/empty-and-minimal/index.html index 19937282..78b8a919 100644 --- a/web/public/th/presets/empty-and-minimal/index.html +++ b/web/public/th/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ ไฟล์ทดสอบที่ถูกต้องเล็กที่สุดและไฟล์ว่างในทุกรูปแบบ - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

      ในชุดมีอะไร

      - - - - + + + +
      ไฟล์31
      target ในสูตรของมัน31
      ขนาดรวม40 138 B
      รูปแบบavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      ไฟล์32
      target ในสูตรของมัน32
      ขนาดรวม40 427 B
      รูปแบบavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -192,7 +192,7 @@

      ในชุดมีอะไร

      ที่คาดหวังความหมายไฟล์ - acceptระบบของคุณควรยอมรับไฟล์นี้29 + acceptระบบของคุณควรยอมรับไฟล์นี้30 unspecifiedขึ้นอยู่กับกฎของระบบของคุณ คุณเป็นผู้ตัดสิน แล้วตรวจสอบว่าสิ่งที่เกิดขึ้นตรงกับที่ตั้งใจ2 diff --git a/web/public/tr/bicimler/index.html b/web/public/tr/bicimler/index.html index 7a32b13b..94fcdd84 100644 --- a/web/public/tr/bicimler/index.html +++ b/web/public/tr/bicimler/index.html @@ -4,8 +4,8 @@ -29 desteklenen dosya biçimi - PDF, DOCX, PNG, ZIP ve daha fazlası - +30 desteklenen dosya biçimi - PDF, DOCX, PNG, ZIP ve daha fazlası + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
      -

      29 dosya biçimi, her biri tam boyutta üretilir

      +

      30 dosya biçimi, her biri tam boyutta üretilir

      Bunların her biri o biçimde gerçek bir dosyadır. Ait olduğu programda açılır ve tam istediğiniz bayt sayısındadır. Hiçbiri yapıştırılmış uzantılı dolgu sıfırları değildir. @@ -195,6 +195,14 @@

      29 dosya biçimi, her biri tam boyutta üretilir

      full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -505,6 +513,46 @@

      Her biçimin kabul ettiği ayarlar

      paragraphs 1 - 50000 paragraf + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + tfg formats'ın listelediği gibi bir biçimin kimliği + + + + attachment_size + 2mb gibi bir boyut + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1008,7 +1056,7 @@

      Her biçimin kabul ettiği ayarlar

      Arşivler gerçek dosyalar içerir

      - targz ve zip boş + eml ve targz ve zip boş bir kabuk olarak bırakılmak yerine girdilerle doldurulabilir. Üretilmiş bir arşiv, içerdiğini söylediği belgeleri gerçekten içerir, bu yüzden bir test sırasında onu açan her şey içinde gerçek dosyalar bulur. diff --git a/web/public/tr/bozuk-test-dosyalari/index.html b/web/public/tr/bozuk-test-dosyalari/index.html index 3f466f09..818060e3 100644 --- a/web/public/tr/bozuk-test-dosyalari/index.html +++ b/web/public/tr/bozuk-test-dosyalari/index.html @@ -201,7 +201,7 @@

      Hasarlı bir dosya yine istediğiniz boyuttadır

      tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      Ayarlar iki noktadan sonra yazılır. Seçenek tekrarlanabilir ve hasarlar yazdığınız sırayla - uygulanır. 29 biçimin her biriyle çalışır. + uygulanır. 30 biçimin her biriyle çalışır.

      diff --git a/web/public/tr/hazir-ayarlar/empty-and-minimal/index.html b/web/public/tr/hazir-ayarlar/empty-and-minimal/index.html index c2c35f89..679ea8e9 100644 --- a/web/public/tr/hazir-ayarlar/empty-and-minimal/index.html +++ b/web/public/tr/hazir-ayarlar/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Her biçimde en küçük geçerli ve boş test dosyaları - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -179,10 +179,10 @@

      Sette ne var?

      - - - - + + + +
      Dosya31
      Tarifindeki hedefler31
      Toplam boyut40 138 B
      Biçimleravif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Dosya32
      Tarifindeki hedefler32
      Toplam boyut40 427 B
      Biçimleravif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -193,7 +193,7 @@

      Sette ne var?

      BeklenenAnlamıDosya - acceptSisteminiz dosyayı kabul etmeli.29 + acceptSisteminiz dosyayı kabul etmeli.30 unspecifiedSisteminizin kurallarına bağlı. Siz karar verirsiniz, sonra olanın amaçladığınız şey olduğunu doğrularsınız.2 diff --git a/web/public/tr/index.html b/web/public/tr/index.html index fa1e802a..a6485a33 100644 --- a/web/public/tr/index.html +++ b/web/public/tr/index.html @@ -4,7 +4,7 @@ -QA için test dosyası oluşturucu - tam boyut, 29 gerçek biçim +QA için test dosyası oluşturucu - tam boyut, 30 gerçek biçim @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "QA için ücretsiz ve açık kaynaklı bir test dosyası oluşturucu. 29 biçimde tam boyutta gerçek dosyalar üretir ve test edilen sistemin her birine nasıl tepki vermesi gerektiğini söyleyen bir manifest yazar.", + "description": "QA için ücretsiz ve açık kaynaklı bir test dosyası oluşturucu. 30 biçimde tam boyutta gerçek dosyalar üretir ve test edilen sistemin her birine nasıl tepki vermesi gerektiğini söyleyen bir manifest yazar.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

      Tam boyutta gerçek test dosyaları üretin

      - PDF, PNG, DOCX, ZIP - toplam 29 biçim ve her biri ait olduğu + PDF, PNG, DOCX, ZIP - toplam 30 biçim ve her biri ait olduğu programda açılan, tam istediğiniz boyutta gerçek bir dosya. Her çalıştırma ayrıca uygulamanızın her dosyayla ne yapması gerektiğini de yazar. Komut satırı ve masaüstü penceresi, ücretsiz ve açık kaynak, tamamen makinenizde çalışır. @@ -179,7 +179,7 @@

      Tam boyutta gerçek test dosyaları üretin

      • - 29 + 30

        gerçek biçim, her biri ait olduğu programda açılır

      • @@ -342,7 +342,7 @@

        Gözetimsiz çalışan bir test takımı için yapıldı

        10485761 bayt isteyin ve tam olarak onu alın. Bir biçimin ulaşamayacağı boyut nedeni olan bir hatadır, asla yanlış boyutta bir dosya değil.

      • -

        29 gerçek biçim

        +

        30 gerçek biçim

        Uzantılı dolgu sıfırları değil. Üretilmiş bir PNG resim görüntüleyicide, bir DOCX Word'de açılır, bir ZIP çıkarılır. Her biri yayımlanmadan önce bağımsız okuyucularla doğrulanır.

      • diff --git a/web/public/tr/sss/index.html b/web/public/tr/sss/index.html index e9673683..7949e6d4 100644 --- a/web/public/tr/sss/index.html +++ b/web/public/tr/sss/index.html @@ -205,7 +205,7 @@

        Sık sorulan sorular

        Sırada hangi biçimler var?

        -

        7z ve mp3. Bugün 29 biçim uçtan uca çalışıyor.

        +

        7z ve mp3. Bugün 30 biçim uçtan uca çalışıyor.

        @@ -276,7 +276,7 @@

        Sık sorulan sorular

        { "@type": "Question", "name": "Sırada hangi biçimler var?", - "acceptedAnswer": { "@type": "Answer", "text": "7z ve mp3. Bugün 29 biçim uçtan uca çalışıyor." } + "acceptedAnswer": { "@type": "Answer", "text": "7z ve mp3. Bugün 30 biçim uçtan uca çalışıyor." } }, { "@type": "Question", diff --git a/web/public/uk/corrupt-test-files/index.html b/web/public/uk/corrupt-test-files/index.html index 4f3c8225..eab28dd0 100644 --- a/web/public/uk/corrupt-test-files/index.html +++ b/web/public/uk/corrupt-test-files/index.html @@ -202,7 +202,7 @@

        Пошкоджений файл лишається потрібного ро tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

        Налаштування пишуться після двокрапки. Параметр можна повторювати, а пошкодження застосовуються в - тому порядку, в якому ви їх записали. Це працює з кожним із 29 форматів. + тому порядку, в якому ви їх записали. Це працює з кожним із 30 форматів.

    diff --git a/web/public/uk/faq/index.html b/web/public/uk/faq/index.html index 9f7eec81..ca524b56 100644 --- a/web/public/uk/faq/index.html +++ b/web/public/uk/faq/index.html @@ -206,7 +206,7 @@

    Часті запитання

    Які формати з'являться далі?

    -

    7z і mp3. Сьогодні повністю працюють 29 форматів.

    +

    7z і mp3. Сьогодні повністю працюють 30 форматів.

    @@ -277,7 +277,7 @@

    Часті запитання

    { "@type": "Question", "name": "Які формати з\u0027являться далі?", - "acceptedAnswer": { "@type": "Answer", "text": "7z і mp3. Сьогодні повністю працюють 29 форматів." } + "acceptedAnswer": { "@type": "Answer", "text": "7z і mp3. Сьогодні повністю працюють 30 форматів." } }, { "@type": "Question", diff --git a/web/public/uk/formats/index.html b/web/public/uk/formats/index.html index 85de0331..8648c925 100644 --- a/web/public/uk/formats/index.html +++ b/web/public/uk/formats/index.html @@ -4,8 +4,8 @@ -29 форматів файлів - PDF, DOCX, PNG, ZIP та інші - +30 форматів файлів - PDF, DOCX, PNG, ZIP та інші + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 форматів файлів, кожен створюється точного розміру

    +

    30 форматів файлів, кожен створюється точного розміру

    Кожен із них - справжній файл цього формату. Він відкривається у своїй програмі та має рівно стільки байтів, скільки ви запросили. Жоден не є нулями-заповнювачами з приклеєним @@ -196,6 +196,14 @@

    29 форматів файлів, кожен створюється точн full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -505,6 +513,46 @@

    Налаштування, які приймає кожен формат

    paragraphs 1 - 50000 абзаців + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + id формату в тому вигляді, як його виводить tfg formats + + + + attachment_size + розмір, наприклад 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1008,7 +1056,7 @@

    Налаштування, які приймає кожен формат

    Архіви містять справжні файли

    - targz і zip + eml і targz і zip можна наповнити записами, а не лишати порожньою оболонкою. Створений архів справді містить документи, які заявляє, тому все, що розпаковує його під час тесту, знаходить усередині справжні файли. diff --git a/web/public/uk/index.html b/web/public/uk/index.html index 5eec5568..fe5ff9c8 100644 --- a/web/public/uk/index.html +++ b/web/public/uk/index.html @@ -4,7 +4,7 @@ -Генератор тестових файлів для QA - точний розмір, 29 форматів +Генератор тестових файлів для QA - точний розмір, 30 форматів @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Безкоштовний генератор тестових файлів для QA з відкритим кодом. Створює справжні файли у 29 форматах точного розміру й записує маніфест з очікуваною реакцією тестованої системи на кожен із них.", + "description": "Безкоштовний генератор тестових файлів для QA з відкритим кодом. Створює справжні файли у 30 форматах точного розміру й записує маніфест з очікуваною реакцією тестованої системи на кожен із них.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

    Створюйте справжні тестові файли точного розміру

    - PDF, PNG, DOCX, ZIP - усього 29 форматів, і кожен із них - + PDF, PNG, DOCX, ZIP - усього 30 форматів, і кожен із них - справжній файл, що відкривається у своїй програмі, точно того розміру, який ви запросили. Кожен запуск ще й записує, що ваш застосунок має робити з кожним файлом. Командний рядок і десктопне вікно, безкоштовно та з відкритим кодом, усе працює на вашій машині. @@ -179,7 +179,7 @@

    Створюйте справжні тестові файли точного
    • - 29 + 30

      справжніх форматів, кожен відкривається у своїй програмі

    • @@ -342,7 +342,7 @@

      Створений для набору тестів, що працює бе

      Запросіть 10485761 байт і отримайте рівно стільки. Розмір, якого формат не може досягти, - це помилка з причиною, а не файл неправильного розміру.

    • -

      29 справжніх форматів

      +

      30 справжніх форматів

      Не нулі-заповнювачі з розширенням. Створений PNG відкривається в переглядачі зображень, DOCX - у Word, ZIP розпаковується. Кожен формат перевіряється незалежними читачами до випуску.

    • diff --git a/web/public/uk/presets/empty-and-minimal/index.html b/web/public/uk/presets/empty-and-minimal/index.html index 49485417..202b8adc 100644 --- a/web/public/uk/presets/empty-and-minimal/index.html +++ b/web/public/uk/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Мінімальні коректні та порожні тестові файли в кожному форматі - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

      Що входить у набір?

      - - - - + + + +
      Файлів31
      Цілей у його рецепті31
      Загальний розмір40 138 B
      Форматиavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      Файлів32
      Цілей у його рецепті32
      Загальний розмір40 427 B
      Форматиavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -192,7 +192,7 @@

      Що входить у набір?

      ОчікуєтьсяЗначенняФайлів - acceptВаша система має прийняти файл.29 + acceptВаша система має прийняти файл.30 unspecifiedЗалежить від правил вашої системи. Ви вирішуєте, а потім перевіряєте, що відбувається саме те, що ви мали на увазі.2 diff --git a/web/public/vi/dinh-dang/index.html b/web/public/vi/dinh-dang/index.html index e2e3a9e7..5564dbf2 100644 --- a/web/public/vi/dinh-dang/index.html +++ b/web/public/vi/dinh-dang/index.html @@ -4,8 +4,8 @@ -29 định dạng tệp được hỗ trợ - PDF, DOCX, PNG, ZIP và hơn nữa - +30 định dạng tệp được hỗ trợ - PDF, DOCX, PNG, ZIP và hơn nữa + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
      -

      29 định dạng tệp, mỗi định dạng được tạo ở kích thước chính xác

      +

      30 định dạng tệp, mỗi định dạng được tạo ở kích thước chính xác

      Mỗi cái là một tệp thật của định dạng đó. Nó mở được bằng phần mềm của nó và có đúng số byte bạn đã yêu cầu. Không cái nào là số không độn với phần mở rộng dán vào. @@ -195,6 +195,14 @@

      29 định dạng tệp, mỗi định dạng được tạo ở kích thư full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -503,6 +511,46 @@

      Các thiết lập mỗi định dạng nhận

      paragraphs 1 - 50000 đoạn văn + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + id của một định dạng, như tfg formats liệt kê + + + + attachment_size + kích thước như 2mb + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -1006,7 +1054,7 @@

      Các thiết lập mỗi định dạng nhận

      Tệp nén chứa tệp thật

      - targz và zip có + eml và targz và zip có thể được nhồi các mục thay vì để thành cái vỏ rỗng. Tệp nén được tạo thật sự chứa các tài liệu nó tuyên bố chứa, nên bất cứ thứ gì giải nén nó trong lúc kiểm thử đều thấy tệp thật bên trong.

      diff --git a/web/public/vi/faq/index.html b/web/public/vi/faq/index.html index 89d3b895..d39a4bd3 100644 --- a/web/public/vi/faq/index.html +++ b/web/public/vi/faq/index.html @@ -206,7 +206,7 @@

      Câu hỏi thường gặp

      Những định dạng nào sắp có?

      -

      7z và mp3. Hôm nay có 29 định dạng hoạt động trọn vẹn từ đầu đến cuối.

      +

      7z và mp3. Hôm nay có 30 định dạng hoạt động trọn vẹn từ đầu đến cuối.

      @@ -277,7 +277,7 @@

      Câu hỏi thường gặp

      { "@type": "Question", "name": "Những định dạng nào sắp có?", - "acceptedAnswer": { "@type": "Answer", "text": "7z và mp3. Hôm nay có 29 định dạng hoạt động trọn vẹn từ đầu đến cuối." } + "acceptedAnswer": { "@type": "Answer", "text": "7z và mp3. Hôm nay có 30 định dạng hoạt động trọn vẹn từ đầu đến cuối." } }, { "@type": "Question", diff --git a/web/public/vi/index.html b/web/public/vi/index.html index b8be52ae..be60157c 100644 --- a/web/public/vi/index.html +++ b/web/public/vi/index.html @@ -4,7 +4,7 @@ -Trình tạo tệp kiểm thử cho QA - kích thước chính xác, 29 định dạng +Trình tạo tệp kiểm thử cho QA - kích thước chính xác, 30 định dạng @@ -33,7 +33,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -76,7 +76,7 @@ "@type": "SoftwareApplication", "name": "Testing Files Generator", "alternateName": "tfg", - "description": "Trình tạo tệp kiểm thử miễn phí, mã nguồn mở cho QA. Nó tạo tệp thật ở 29 định dạng, đúng kích thước, và ghi manifest nêu cách hệ thống được kiểm thử cần phản ứng với từng tệp.", + "description": "Trình tạo tệp kiểm thử miễn phí, mã nguồn mở cho QA. Nó tạo tệp thật ở 30 định dạng, đúng kích thước, và ghi manifest nêu cách hệ thống được kiểm thử cần phản ứng với từng tệp.", "url": "https:\/\/testingfilesgenerator.donislawdev.com/", "applicationCategory": "DeveloperApplication", "applicationSubCategory": "Software Testing", @@ -157,7 +157,7 @@

      Tạo tệp kiểm thử thật đúng kích thước

      - PDF, PNG, DOCX, ZIP - tổng cộng 29 định dạng, và mỗi tệp là + PDF, PNG, DOCX, ZIP - tổng cộng 30 định dạng, và mỗi tệp là tệp thật mở được bằng phần mềm của nó, ở đúng kích thước bạn yêu cầu. Mỗi lần chạy còn ghi lại ứng dụng của bạn cần làm gì với từng tệp. Dòng lệnh và cửa sổ desktop, miễn phí, mã nguồn mở, chạy hoàn toàn trên máy của bạn. @@ -179,7 +179,7 @@

      Tạo tệp kiểm thử thật đúng kích thước

      • - 29 + 30

        định dạng thật, mỗi định dạng mở được bằng phần mềm của nó

      • @@ -341,7 +341,7 @@

        Làm cho bộ kiểm thử chạy không cần người trông

        Yêu cầu 10485761 byte và nhận đúng như vậy. Kích thước mà định dạng không thể đạt là một lỗi có lý do, không bao giờ là tệp sai kích thước.

      • -

        29 định dạng thật

        +

        30 định dạng thật

        Không phải số không độn kèm một phần mở rộng. PNG được tạo mở trong trình xem ảnh, DOCX mở trong Word, ZIP giải nén được. Mỗi định dạng được kiểm tra bằng các trình đọc độc lập trước khi phát hành.

      • diff --git a/web/public/vi/preset/empty-and-minimal/index.html b/web/public/vi/preset/empty-and-minimal/index.html index 14a7080b..1ec9ec9c 100644 --- a/web/public/vi/preset/empty-and-minimal/index.html +++ b/web/public/vi/preset/empty-and-minimal/index.html @@ -5,7 +5,7 @@ Tệp kiểm thử hợp lệ nhỏ nhất và tệp rỗng ở mọi định dạng - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -178,10 +178,10 @@

        Trong bộ có gì?

        - - - - + + + +
        Tệp31
        Target trong công thức của nó31
        Tổng kích thước40 138 B
        Định dạngavif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
        Tệp32
        Target trong công thức của nó32
        Tổng kích thước40 427 B
        Định dạngavif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
        @@ -192,7 +192,7 @@

        Trong bộ có gì?

        Mong đợiÝ nghĩaTệp - acceptHệ thống của bạn nên nhận tệp.29 + acceptHệ thống của bạn nên nhận tệp.30 unspecifiedTùy vào quy tắc của hệ thống bạn. Bạn quyết định, rồi kiểm tra điều xảy ra có đúng ý bạn không.2 diff --git a/web/public/vi/tep-kiem-thu-bi-hong/index.html b/web/public/vi/tep-kiem-thu-bi-hong/index.html index 4620dc04..191ad730 100644 --- a/web/public/vi/tep-kiem-thu-bi-hong/index.html +++ b/web/public/vi/tep-kiem-thu-bi-hong/index.html @@ -199,7 +199,7 @@

        Tệp bị hỏng vẫn có kích thước bạn đã yêu cầu

        tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

        Cài đặt viết sau dấu hai chấm. Tùy chọn có thể lặp lại, và các hỏng hóc được áp dụng theo thứ tự bạn - viết. Nó dùng được với từng định dạng trong 29 định dạng. + viết. Nó dùng được với từng định dạng trong 30 định dạng.

      diff --git a/web/public/zh-hans/corrupt-test-files/index.html b/web/public/zh-hans/corrupt-test-files/index.html index 38d47ad0..a91263e0 100644 --- a/web/public/zh-hans/corrupt-test-files/index.html +++ b/web/public/zh-hans/corrupt-test-files/index.html @@ -189,7 +189,7 @@

      损坏的文件仍然是你要求的大小

      tfg generate --format png --size 2mb --damage zero-head --out ./out
       tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      - 设置写在冒号之后。该选项可以重复,损坏按你写下的顺序依次应用。它适用于全部 29 种格式。 + 设置写在冒号之后。该选项可以重复,损坏按你写下的顺序依次应用。它适用于全部 30 种格式。

    diff --git a/web/public/zh-hans/faq/index.html b/web/public/zh-hans/faq/index.html index 06022f8a..8ad6c7d7 100644 --- a/web/public/zh-hans/faq/index.html +++ b/web/public/zh-hans/faq/index.html @@ -204,7 +204,7 @@

    常见问题

    接下来会支持哪些格式?

    -

    7z 和 mp3。目前有 29 种格式可以端到端使用。

    +

    7z 和 mp3。目前有 30 种格式可以端到端使用。

    @@ -275,7 +275,7 @@

    常见问题

    { "@type": "Question", "name": "接下来会支持哪些格式?", - "acceptedAnswer": { "@type": "Answer", "text": "7z 和 mp3。目前有 29 种格式可以端到端使用。" } + "acceptedAnswer": { "@type": "Answer", "text": "7z 和 mp3。目前有 30 种格式可以端到端使用。" } }, { "@type": "Question", diff --git a/web/public/zh-hans/formats/index.html b/web/public/zh-hans/formats/index.html index 35712a41..9e5afa67 100644 --- a/web/public/zh-hans/formats/index.html +++ b/web/public/zh-hans/formats/index.html @@ -4,8 +4,8 @@ -29 种支持的文件格式 - PDF、DOCX、PNG、ZIP 等 - +30 种支持的文件格式 - PDF、DOCX、PNG、ZIP 等 + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 种文件格式,每一种都按精确大小生成

    +

    30 种文件格式,每一种都按精确大小生成

    它们每一个都是该格式的真实文件。它能在对应软件中打开,字节数恰好等于你的要求。没有一个是粘上扩展名的填充零。

    @@ -194,6 +194,14 @@

    29 种文件格式,每一种都按精确大小生成

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -497,6 +505,46 @@

    每种格式接受的设置

    paragraphs 1 - 50000 段落 + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + 格式的 id,与 tfg formats 列出的一致 + + + + attachment_size + 形如 2mb 的大小 + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -996,7 +1044,7 @@

    每种格式接受的设置

    压缩包里装的是真实文件

    - targz 和 zip + eml 和 targz 和 zip 可以填入条目,而不是只留一个空壳。生成的压缩包确实包含它声称包含的文档,所以测试期间解压它的任何程序都会在里面找到真实文件。

    targets:
    diff --git a/web/public/zh-hans/index.html b/web/public/zh-hans/index.html
    index b69bcf94..90ace16c 100644
    --- a/web/public/zh-hans/index.html
    +++ b/web/public/zh-hans/index.html
    @@ -4,7 +4,7 @@
     
     
     
    -测试文件生成器 - 精确大小,29 种真实格式
    +测试文件生成器 - 精确大小,30 种真实格式
     
     
     
    @@ -33,7 +33,7 @@
     
     
     
    -
    +
     
     
     
    @@ -62,7 +62,7 @@
     
     
     
    -
    +
     
     
     
    @@ -76,7 +76,7 @@
       "@type": "SoftwareApplication",
       "name": "Testing Files Generator",
       "alternateName": "tfg",
    -  "description": "面向 QA 的免费开源测试文件生成器。它生成 29 种格式、大小精确的真实文件,并写出说明被测系统应如何响应每个文件的清单。",
    +  "description": "面向 QA 的免费开源测试文件生成器。它生成 30 种格式、大小精确的真实文件,并写出说明被测系统应如何响应每个文件的清单。",
       "url": "https:\/\/testingfilesgenerator.donislawdev.com/",
       "applicationCategory": "DeveloperApplication",
       "applicationSubCategory": "Software Testing",
    @@ -157,7 +157,7 @@
       

    生成大小精确的真实测试文件

    - PDF、PNG、DOCX、ZIP,共 29 + PDF、PNG、DOCX、ZIP,共 30 种格式,每一种都是能在对应软件中打开的真实文件,大小恰好等于你的要求。每次运行还会写下你的应用应该如何处理每个文件。命令行加桌面窗口,免费开源,完全在你的机器上运行。

    @@ -177,7 +177,7 @@

    生成大小精确的真实测试文件

    • - 29 + 30

      种真实格式,每一种都能在对应软件中打开

    • @@ -332,7 +332,7 @@

      为无人值守的测试套件而生

      请求 10485761 字节,就得到恰好这么多。格式无法达到的大小会得到带原因的错误,绝不会得到大小错误的文件。

    • -

      29 种真实格式

      +

      30 种真实格式

      不是带扩展名的填充零。生成的 PNG 能在图片查看器中打开,DOCX 能在 Word 中打开,ZIP 能解压。每种格式在发布前都经过独立读取器验证。

    • diff --git a/web/public/zh-hans/presets/empty-and-minimal/index.html b/web/public/zh-hans/presets/empty-and-minimal/index.html index 3a517f1a..fd49d0df 100644 --- a/web/public/zh-hans/presets/empty-and-minimal/index.html +++ b/web/public/zh-hans/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ 各格式最小的合法文件与空文件 - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -177,10 +177,10 @@

      集合里有什么?

      - - - - + + + +
      文件数31
      其配方中的 target 数31
      总大小40 138 B
      格式avif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      文件数32
      其配方中的 target 数32
      总大小40 427 B
      格式avif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -191,7 +191,7 @@

      集合里有什么?

      预期含义文件数 - accept你的系统应该接受这个文件。29 + accept你的系统应该接受这个文件。30 unspecified取决于你的系统规则。由你决定,然后检查实际发生的是否符合你的本意。2 diff --git a/web/public/zh-hant/corrupt-test-files/index.html b/web/public/zh-hant/corrupt-test-files/index.html index fc54336f..ab36352b 100644 --- a/web/public/zh-hant/corrupt-test-files/index.html +++ b/web/public/zh-hant/corrupt-test-files/index.html @@ -189,7 +189,7 @@

      損毀的檔案仍然是你要求的大小

      tfg generate --format png --size 2mb --damage zero-head --out ./out
       tfg generate --format pdf --size 1mb --count 5 --damage zero-head:bytes=16 --out ./broken

      - 設定寫在冒號之後。該選項可以重複,損毀依你寫下的順序逐一套用。它適用於全部 29 種格式。 + 設定寫在冒號之後。該選項可以重複,損毀依你寫下的順序逐一套用。它適用於全部 30 種格式。

    diff --git a/web/public/zh-hant/faq/index.html b/web/public/zh-hant/faq/index.html index bfde6de9..9d00d874 100644 --- a/web/public/zh-hant/faq/index.html +++ b/web/public/zh-hant/faq/index.html @@ -204,7 +204,7 @@

    常見問題

    接下來會支援哪些格式?

    -

    7z 與 mp3。目前有 29 種格式可以端到端使用。

    +

    7z 與 mp3。目前有 30 種格式可以端到端使用。

    @@ -275,7 +275,7 @@

    常見問題

    { "@type": "Question", "name": "接下來會支援哪些格式?", - "acceptedAnswer": { "@type": "Answer", "text": "7z 與 mp3。目前有 29 種格式可以端到端使用。" } + "acceptedAnswer": { "@type": "Answer", "text": "7z 與 mp3。目前有 30 種格式可以端到端使用。" } }, { "@type": "Question", diff --git a/web/public/zh-hant/formats/index.html b/web/public/zh-hant/formats/index.html index 9fa71e54..97983efb 100644 --- a/web/public/zh-hant/formats/index.html +++ b/web/public/zh-hant/formats/index.html @@ -4,8 +4,8 @@ -29 種支援的檔案格式 - PDF、DOCX、PNG、ZIP 等 - +30 種支援的檔案格式 - PDF、DOCX、PNG、ZIP 等 + @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + @@ -144,7 +144,7 @@
    -

    29 種檔案格式,每一種都以精確大小產生

    +

    30 種檔案格式,每一種都以精確大小產生

    它們每一個都是該格式的真實檔案。它能在對應軟體中開啟,位元組數恰好等於你的要求。沒有一個是黏上副檔名的填充零。

    @@ -194,6 +194,14 @@

    29 種檔案格式,每一種都以精確大小產生

    full libreoffice + + eml + Email message + .eml + 289 + full + python-email + geojson GeoJSON @@ -497,6 +505,46 @@

    每種格式接受的設定

    paragraphs 1 - 50000 段落 + + eml + body + alternative, html, plain + + + + text_encoding + 7bit, 8bit, quoted-printable + + + + headers + ascii, encoded, utf8 + + + + line_endings + crlf, lf + + + + attachments + 0 - 10000 + + + + attachment_format + 格式的 id,與 tfg formats 列出的相同 + + + + attachment_size + 形如 2mb 的大小 + + + + filename_style + both, content-type, rfc2047, rfc2231 + geojson geometry @@ -996,7 +1044,7 @@

    每種格式接受的設定

    壓縮檔裡裝的是真實檔案

    - targz 與 zip + eml 與 targz 與 zip 可以填入項目,而不是只留一個空殼。產生的壓縮檔確實包含它宣稱包含的文件,所以測試期間解壓縮它的任何程式都會在裡面找到真實檔案。

    targets:
    diff --git a/web/public/zh-hant/index.html b/web/public/zh-hant/index.html
    index 35de19d1..f2e97648 100644
    --- a/web/public/zh-hant/index.html
    +++ b/web/public/zh-hant/index.html
    @@ -4,7 +4,7 @@
     
     
     
    -測試檔案產生器 - 精確大小,29 種真實格式
    +測試檔案產生器 - 精確大小,30 種真實格式
     
     
     
    @@ -33,7 +33,7 @@
     
     
     
    -
    +
     
     
     
    @@ -62,7 +62,7 @@
     
     
     
    -
    +
     
     
     
    @@ -76,7 +76,7 @@
       "@type": "SoftwareApplication",
       "name": "Testing Files Generator",
       "alternateName": "tfg",
    -  "description": "專為 QA 設計的免費開源測試檔案產生器。它產生 29 種格式、大小精確的真實檔案,並寫出說明受測系統應如何回應每個檔案的清單。",
    +  "description": "專為 QA 設計的免費開源測試檔案產生器。它產生 30 種格式、大小精確的真實檔案,並寫出說明受測系統應如何回應每個檔案的清單。",
       "url": "https:\/\/testingfilesgenerator.donislawdev.com/",
       "applicationCategory": "DeveloperApplication",
       "applicationSubCategory": "Software Testing",
    @@ -157,7 +157,7 @@
       

    產生大小精確的真實測試檔案

    - PDF、PNG、DOCX、ZIP,共 29 + PDF、PNG、DOCX、ZIP,共 30 種格式,每一種都是能在對應軟體中開啟的真實檔案,大小恰好等於你的要求。每次執行還會寫下你的應用程式應該如何處理每個檔案。命令列加桌面視窗,免費開源,完全在你的電腦上執行。

    @@ -177,7 +177,7 @@

    產生大小精確的真實測試檔案

    • - 29 + 30

      種真實格式,每一種都能在對應軟體中開啟

    • @@ -332,7 +332,7 @@

      為無人看管的測試套件而生

      要求 10485761 位元組,就得到恰好這麼多。格式無法達到的大小會得到附上原因的錯誤,絕不會得到大小錯誤的檔案。

    • -

      29 種真實格式

      +

      30 種真實格式

      不是帶副檔名的填充零。產生的 PNG 能在圖片檢視器中開啟,DOCX 能在 Word 中開啟,ZIP 能解壓縮。每種格式在發布前都經過獨立讀取器驗證。

    • diff --git a/web/public/zh-hant/presets/empty-and-minimal/index.html b/web/public/zh-hant/presets/empty-and-minimal/index.html index d2840486..c51ef6d8 100644 --- a/web/public/zh-hant/presets/empty-and-minimal/index.html +++ b/web/public/zh-hant/presets/empty-and-minimal/index.html @@ -5,7 +5,7 @@ 各格式最小的合法檔案與空檔案 - + @@ -34,7 +34,7 @@ - + @@ -63,7 +63,7 @@ - + @@ -177,10 +177,10 @@

      組合裡有什麼?

      - - - - + + + +
      檔案數31
      其配方中的 target 數31
      總大小40 138 B
      格式avif, bmp, csv, docx, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      檔案數32
      其配方中的 target 數32
      總大小40 427 B
      格式avif, bmp, csv, docx, eml, geojson, gif, html, ico, jpg, json, jxl, log, md, mp4, pdf, png, pptx, svg, targz, tiff, toml, txt, wav, webm, webp, xlsx, xml, yaml, zip
      @@ -191,7 +191,7 @@

      組合裡有什麼?

      預期意義檔案數 - accept你的系統應該接受這個檔案。29 + accept你的系統應該接受這個檔案。30 unspecified取決於你的系統規則。由你決定,然後檢查實際發生的是否符合你的本意。2 diff --git a/web/social-preview.sha256 b/web/social-preview.sha256 index a50da7b6..8c32e1c0 100644 --- a/web/social-preview.sha256 +++ b/web/social-preview.sha256 @@ -1 +1 @@ -997f5b7071c1ea6b9daf72d4a1cf4bc1c52a44ddfe28b2c76771bcf3d2baac8d +a7706bfec4f2b97cebbc7bc3e3a40dd188b0279f46a94564730973300a61c772