Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02942b591d |
@@ -250,14 +250,15 @@ identity's own recipient, plus any given with `--to`, so the same mnemonic can
|
|||||||
always decrypt what it encrypted. Output goes to `-o` or standard output;
|
always decrypt what it encrypted. Output goes to `-o` or standard output;
|
||||||
`--armor` writes the text form. Nothing is written except the output.
|
`--armor` writes the text form. Nothing is written except the output.
|
||||||
|
|
||||||
A regular file already at the `-o` path is replaced, and the new file has mode
|
A `-o` path that is the same file as the tool's own standard output or standard
|
||||||
`0600`. A symlink there is followed, and what it points at is treated the same
|
error, under any name such as `/dev/stdout` or `/dev/fd/2`, is written to that
|
||||||
way, so the link keeps pointing where it did; a symlink that points at nothing
|
stream, as leaving out `-o` writes to standard output; the file the stream is
|
||||||
is refused. A named pipe or a device, such as `/dev/null`, is written to
|
redirected to is written as the redirect says and never replaced, so with `>>`
|
||||||
directly. `-o /dev/stdout` writes to the tool's own standard output, as leaving
|
the output follows what the file already held. Otherwise, a regular file already
|
||||||
out `-o` does, and `-o /dev/stderr` to its standard error; a file either stream
|
at the `-o` path is replaced, and the new file has mode `0600`. A symlink there
|
||||||
is redirected to is written as the redirect says and never replaced, so with
|
is followed, and what it points at is treated the same way, so the link keeps
|
||||||
`>>` the output follows what the file already held.
|
pointing where it did; a symlink that points at nothing is refused. A named pipe
|
||||||
|
or a device, such as `/dev/null`, is written to directly.
|
||||||
|
|
||||||
### `keyfunc age decrypt [-n N] [-o <file>] [<file>]`
|
### `keyfunc age decrypt [-n N] [-o <file>] [<file>]`
|
||||||
|
|
||||||
@@ -306,12 +307,12 @@ already at the named path as it was, and exit with status 1. That holds for a
|
|||||||
signal that has reached `keyfunc` when its input ends; a later one leaves the
|
signal that has reached `keyfunc` when its input ends; a later one leaves the
|
||||||
whole file in place. Ctrl-C on a pipeline ends the input at the same moment, and
|
whole file in place. Ctrl-C on a pipeline ends the input at the same moment, and
|
||||||
on Linux `keyfunc` sees the signal first, though no system promises that. A
|
on Linux `keyfunc` sees the signal first, though no system promises that. A
|
||||||
named pipe or a device at the `-o` path, `/dev/stdout` or `/dev/stderr`, is
|
named pipe or a device at the `-o` path, or a path that is the same file as
|
||||||
written to directly, and the signal ends the tool there as it ends any other
|
standard output or standard error, is written to directly, and the signal ends
|
||||||
command. While `ssh to` or `ssh install` has `ssh` or `sftp` running, the signal
|
the tool there as it ends any other command. While `ssh to` or `ssh install` has
|
||||||
ends that program instead, the tool removes its agent socket or working files,
|
`ssh` or `sftp` running, the signal ends that program instead, the tool removes
|
||||||
and it exits with status 1, or for `ssh to` with `ssh`'s own status if `ssh`
|
its agent socket or working files, and it exits with status 1, or for `ssh to`
|
||||||
reported one.
|
with `ssh`'s own status if `ssh` reported one.
|
||||||
|
|
||||||
## Entrypoints
|
## Entrypoints
|
||||||
|
|
||||||
|
|||||||
+30
-8
@@ -141,10 +141,10 @@ func runDecrypt(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
// through opens the input the arguments ask for and hands it to the
|
// through opens the input the arguments ask for and hands it to the
|
||||||
// work, with the file --output names to write to, or the command's own
|
// work, with the file --output names to write to, or the command's own
|
||||||
// output when it names none or names /dev/stdout, and the command's own
|
// output when it names none or names the same file as that output, and
|
||||||
// error output when it names /dev/stderr. Those two are the streams the
|
// the command's own error output when it names the same file as that.
|
||||||
// tool already has, so whatever they are redirected to is written as
|
// Those two are the streams the tool already has, so whatever they are
|
||||||
// the redirect says, never replaced.
|
// redirected to is written as the redirect says, never replaced.
|
||||||
func through(
|
func through(
|
||||||
cmd *cobra.Command, args []string,
|
cmd *cobra.Command, args []string,
|
||||||
work func(io.Writer, io.Reader) error,
|
work func(io.Writer, io.Reader) error,
|
||||||
@@ -161,16 +161,38 @@ func through(
|
|||||||
return fmt.Errorf("reading the output file: %w", err)
|
return fmt.Errorf("reading the output file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch name {
|
switch {
|
||||||
case "", "/dev/stdout":
|
case name == "", same(name, cmd.OutOrStdout()):
|
||||||
return work(cmd.OutOrStdout(), src)
|
return work(cmd.OutOrStdout(), src)
|
||||||
case "/dev/stderr":
|
case same(name, cmd.ErrOrStderr()):
|
||||||
return work(cmd.ErrOrStderr(), src)
|
return work(cmd.ErrOrStderr(), src)
|
||||||
default:
|
default:
|
||||||
return output(name, src, work)
|
return output(name, src, work)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// same reports whether the named path, followed to the end, is the
|
||||||
|
// file the stream writes to, whatever name it is reached by, such as
|
||||||
|
// /dev/stdout or /dev/fd/1 for standard output.
|
||||||
|
func same(name string, stream io.Writer) bool {
|
||||||
|
file, ok := stream.(*os.File)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
streamInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.SameFile(info, streamInfo)
|
||||||
|
}
|
||||||
|
|
||||||
// input returns what to read from: the named file, or the command's
|
// input returns what to read from: the named file, or the command's
|
||||||
// own input when no file is named. The second result closes a file
|
// own input when no file is named. The second result closes a file
|
||||||
// that was opened and does nothing otherwise.
|
// that was opened and does nothing otherwise.
|
||||||
@@ -210,7 +232,7 @@ func output(
|
|||||||
case info.Mode().IsRegular():
|
case info.Mode().IsRegular():
|
||||||
return replace(name, src, work)
|
return replace(name, src, work)
|
||||||
case info.Mode().Type() == fs.ModeSymlink:
|
case info.Mode().Type() == fs.ModeSymlink:
|
||||||
// os.Stat follows the link as opening it would. /dev/fd/1
|
// os.Stat follows the link as opening it would. /dev/fd/3
|
||||||
// needs that: it reaches a pipe or a terminal through a link
|
// needs that: it reaches a pipe or a terminal through a link
|
||||||
// that names no path.
|
// that names no path.
|
||||||
info, err = os.Stat(name)
|
info, err = os.Stat(name)
|
||||||
|
|||||||
@@ -151,14 +151,32 @@ func TestANamedPipeAtTheOutputPathIsWrittenToAndStaysAPipe(t *testing.T) {
|
|||||||
require.Equal(t, "the secret\n", run(t, "age", "decrypt", sealedFile))
|
require.Equal(t, "the secret\n", run(t, "age", "decrypt", sealedFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDevStdoutAddsToTheFileStandardOutputIsAppendedTo(t *testing.T) {
|
func TestANameForStandardOutputAddsToTheFileItIsAppendedTo(t *testing.T) {
|
||||||
t.Setenv(mnemonic.Variable, example())
|
t.Setenv(mnemonic.Variable, example())
|
||||||
|
|
||||||
sealed := filepath.Join(t.TempDir(), "notes.age")
|
sealed := filepath.Join(t.TempDir(), "notes.age")
|
||||||
run(t, "age", "encrypt", "-o", sealed, written(t, "notes.txt", "the secret\n"))
|
run(t, "age", "encrypt", "-o", sealed, written(t, "notes.txt", "the secret\n"))
|
||||||
|
|
||||||
// What the shell's ">> notes.out" gives the tool as standard output.
|
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")
|
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
|
//nolint:gosec // the test made this path itself
|
||||||
appended, err := os.OpenFile(existing, os.O_WRONLY|os.O_APPEND, 0)
|
appended, err := os.OpenFile(existing, os.O_WRONLY|os.O_APPEND, 0)
|
||||||
@@ -168,16 +186,21 @@ func TestDevStdoutAddsToTheFileStandardOutputIsAppendedTo(t *testing.T) {
|
|||||||
|
|
||||||
//nolint:gosec // this test's own binary as the tool
|
//nolint:gosec // this test's own binary as the tool
|
||||||
command := exec.CommandContext(
|
command := exec.CommandContext(
|
||||||
t.Context(), os.Args[0], "age", "decrypt", "-o", "/dev/stdout", sealed,
|
t.Context(), os.Args[0], "age", "decrypt", "-o", name, sealed,
|
||||||
)
|
)
|
||||||
|
|
||||||
command.Env = append(os.Environ(), runAsTool+"=1")
|
command.Env = append(os.Environ(), runAsTool+"=1")
|
||||||
command.Stdout = appended
|
command.Stdout = appended
|
||||||
|
|
||||||
require.NoError(t, command.Run())
|
require.NoError(t, command.Run(), name)
|
||||||
require.Equal(t,
|
require.Equal(t,
|
||||||
"what was already there\nthe secret\n", read(t, existing),
|
"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) {
|
func TestASignalStopsAnEncryptionAndLeavesNoFile(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user