age -o follows a symlink and writes a pipe or device directly (closes #59)
check / check (push) Failing after 2s
check / check (push) Failing after 2s
age encrypt -o and age decrypt -o always renamed a new file over the named path, replacing a symlink, a named pipe or a device such as /dev/null with a regular file. A path that is the same file as the tool's standard output or standard error, under any name, is now written to that stream, so the file it is redirected to keeps its contents. Any other path is looked at without following a final symlink: nothing there or a regular file is replaced by rename as before, a symlink gets the same treatment for what it points at and is refused if it points at nothing, and anything else is written to directly, without catching signals. The README says so, and that a replaced file has mode 0600. Model: opus-5-5
This commit is contained in:
@@ -3,6 +3,7 @@ package cli_test
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -98,6 +99,110 @@ func TestARefusedDecryptionLeavesTheOutputFileAlone(t *testing.T) {
|
||||
require.Equal(t, "what was already there\n", string(kept))
|
||||
}
|
||||
|
||||
func TestASymlinkAtTheOutputPathStaysAndItsTargetGetsTheOutput(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
plain := written(t, "notes.txt", "the secret\n")
|
||||
target := written(t, "notes.age", "what was already there\n")
|
||||
link := filepath.Join(t.TempDir(), "notes.age")
|
||||
require.NoError(t, os.Symlink(target, link))
|
||||
|
||||
run(t, "age", "encrypt", "-o", link, plain)
|
||||
|
||||
pointsAt, err := os.Readlink(link)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, target, pointsAt)
|
||||
require.Equal(t, "the secret\n", run(t, "age", "decrypt", target))
|
||||
}
|
||||
|
||||
func TestANamedPipeAtTheOutputPathIsWrittenToAndStaysAPipe(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
plain := written(t, "notes.txt", "the secret\n")
|
||||
pipe := filepath.Join(t.TempDir(), "notes.age")
|
||||
require.NoError(t, syscall.Mkfifo(pipe, fileMode))
|
||||
|
||||
// Opening the pipe to read waits until the tool opens it to write.
|
||||
var sealed []byte
|
||||
|
||||
finished := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
sealed, err = os.ReadFile(pipe) //nolint:gosec // the test's own path
|
||||
finished <- err
|
||||
}()
|
||||
|
||||
run(t, "age", "encrypt", "-o", pipe, plain)
|
||||
|
||||
select {
|
||||
case err := <-finished:
|
||||
require.NoError(t, err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("nothing was written to the pipe")
|
||||
}
|
||||
|
||||
info, err := os.Lstat(pipe)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fs.ModeNamedPipe, info.Mode().Type())
|
||||
|
||||
sealedFile := written(t, "notes.age", string(sealed))
|
||||
require.Equal(t, "the secret\n", run(t, "age", "decrypt", sealedFile))
|
||||
}
|
||||
|
||||
func TestANameForStandardOutputAddsToTheFileItIsAppendedTo(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
sealed := filepath.Join(t.TempDir(), "notes.age")
|
||||
run(t, "age", "encrypt", "-o", sealed, written(t, "notes.txt", "the secret\n"))
|
||||
|
||||
for _, name := range []string{"/dev/stdout", "/dev/fd/1"} {
|
||||
appendedThrough(t, name, sealed)
|
||||
}
|
||||
}
|
||||
|
||||
// appendedThrough decrypts sealed with -o name while the tool's standard
|
||||
// output is appended to a file that already has contents, as the shell's
|
||||
// ">> notes.out" does, and checks that the file is the same one, with
|
||||
// the same mode, and holds its earlier contents and then the output.
|
||||
func appendedThrough(t *testing.T, name, sealed string) {
|
||||
t.Helper()
|
||||
|
||||
// A mode of its own, so that a replaced file would show.
|
||||
const ownMode = 0o644
|
||||
|
||||
existing := written(t, "notes.out", "what was already there\n")
|
||||
require.NoError(t, os.Chmod(existing, ownMode))
|
||||
|
||||
before, err := os.Stat(existing)
|
||||
require.NoError(t, err)
|
||||
|
||||
//nolint:gosec // the test made this path itself
|
||||
appended, err := os.OpenFile(existing, os.O_WRONLY|os.O_APPEND, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() { _ = appended.Close() }()
|
||||
|
||||
//nolint:gosec // this test's own binary as the tool
|
||||
command := exec.CommandContext(
|
||||
t.Context(), os.Args[0], "age", "decrypt", "-o", name, sealed,
|
||||
)
|
||||
|
||||
command.Env = append(os.Environ(), runAsTool+"=1")
|
||||
command.Stdout = appended
|
||||
|
||||
require.NoError(t, command.Run(), name)
|
||||
require.Equal(t,
|
||||
"what was already there\nthe secret\n", read(t, existing), name,
|
||||
)
|
||||
|
||||
after, err := os.Stat(existing)
|
||||
require.NoError(t, err)
|
||||
require.True(t, os.SameFile(before, after), name)
|
||||
require.Equal(t, os.FileMode(ownMode), after.Mode().Perm(), name)
|
||||
}
|
||||
|
||||
func TestASignalStopsAnEncryptionAndLeavesNoFile(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user