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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,45 @@ because it turns other people's test suites red.

### Added

- **A zip can be locked with a password.**

tfg generate --format zip --size 30kb --set entries=3 \
--set password=Secret123 --set encryption=aes-256

writes an archive of exactly 30720 B that 7-Zip opens with that password
and refuses without it. The methods are `aes-128`, `aes-192` and
`aes-256`.

**The password goes into the manifest in plain text.** A locked fixture
nobody can open is worth nothing, so the manifest records it exactly as
you typed it - that is the point rather than a leak. Do not use a password
you use anywhere else.

Both settings are needed together. A password with `encryption=none`, or
an encryption with no password, is refused rather than guessed at, and the
refusal names both of them.

**Limits worth knowing before you build a fixture.** Some readers cannot
open AES archives at all - .NET's own `ZipFile` lists the entries and then
fails on reading one. Nothing in this build writes the older ZipCrypto
scheme yet, so an archive meant for a reader that only speaks that is not
something this can make. And `tar.gz` cannot be locked at all - neither
tar nor gzip has any encryption in it, and asking for one there is refused
with that reason rather than ignored.

- **A tar.gz can say what permissions its files have and who owns them.**
`--set entry_mode=755` and `--set entry_owner=root`. The modes are the
ones chmod takes, from `000` through `777`, and the owners are `unset`
(the default, and what this tool has always written), `root` and `user`.

The useful cases are the ones nobody makes by accident: `000` is a file
nothing can read after unpacking, `777` is one a scanner should have
something to say about, and an archive claiming root owns everything is
what a careless extractor turns into a privilege problem.

It changes no bytes unless you ask for it, and the size of the archive is
the same either way.

- **A log can now be six shapes rather than one, and seven settings shape it.**
`tfg generate --format log --set entry_format=nginx` writes an nginx access
log. The others are `apache-combined` (the default, and what this format has
Expand Down
8 changes: 8 additions & 0 deletions internal/cli/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ func classifyFormat(err error) (int, bool) {
if errors.As(err, &nesting) {
return ExitFormat, true
}
// A setting the format cannot carry is the same class again: the request
// is well formed and this file format has nowhere to put it. Deliberately
// not the code a misspelt key gets - that one is a typo in what somebody
// typed, and this one is true of the format itself.
var unsupported *format.UnsupportedSettingError
if errors.As(err, &unsupported) {
return ExitFormat, true
}
// A value outside what the format declares is a request the format cannot
// deliver, which is what FORMAT means - the same class as a size below the
// minimum. It used to fall through to RUNTIME, so "--set width=abc" told
Expand Down
58 changes: 58 additions & 0 deletions internal/format/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,29 @@ const (
Entries = "entries"
EntryFormat = "entry_format"
EntrySize = "entry_size"
Password = "password"
Encryption = "encryption"
EntryMode = "entry_mode"
EntryOwner = "entry_owner"
)

// The owners an entry can be recorded as.
const (
// OwnerUnset records no owner at all, which is what a tar written here has
// always carried and therefore what the default has to stay - a different
// default would move the bytes of every archive, which is untouchable rule
// 3.
OwnerUnset = "unset"
OwnerRoot = "root"
OwnerUser = "user"
)

// The encryption methods, spelled the way a recipe writes them.
const (
NoEncryption = "none"
AES128 = "aes-128"
AES192 = "aes-192"
AES256 = "aes-256"
)

const (
Expand Down Expand Up @@ -109,6 +132,41 @@ var axes = map[string]format.Property{
Default: defaultSizeText,
Detail: "How big each file inside is.",
},
Password: {
Name: Password, Kind: format.PropertyText,
Shape: "the password, in plain text",
// No default, and that is the point. A box somebody types in arrives
// empty from a window, so leaving it alone is how "no password" is
// said - see the pair rule in readLock.
Detail: "The password the archive is locked with. It is written into the manifest as you typed it, " +
"because a test that cannot open the file cannot check anything.",
},
EntryMode: {
Name: EntryMode, Kind: format.PropertyChoice,
// Written the way chmod takes them, and sorted, because a closed set
// has one order on every surface. The interesting ones for a test are
// at the ends: 000 is a file nothing can read, 444 is read only, and
// 666 and 777 are what a scanner should have something to say about.
Choices: []string{"000", "400", "444", "600", "644", "664", "666", "700", "755", "777"},
Default: "644",
Detail: "The permissions recorded for each file inside. It is what the archive says, " +
"not what the file gets - that depends on who unpacks it and how.",
},
EntryOwner: {
Name: EntryOwner, Kind: format.PropertyChoice,
Choices: []string{OwnerRoot, OwnerUnset, OwnerUser},
Default: OwnerUnset,
Detail: "Who each file inside belongs to. Leave it unset and the archive names nobody, " +
"which is what most archives written by a build carry.",
},
Encryption: {
Name: Encryption, Kind: format.PropertyChoice,
// Sorted, because a closed set has one order on every surface.
Choices: []string{AES128, AES192, AES256, NoEncryption},
Default: NoEncryption,
Detail: "How the archive is locked. This is the WinZip AES scheme, which 7-Zip and WinZip open " +
"and some other readers cannot open at all - .NET lists the files and then fails on reading one.",
},
}

// Names is every container setting this build declares, in a stable order.
Expand Down
Loading
Loading