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:
+105
-5
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -140,7 +141,10 @@ func runDecrypt(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// 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
|
||||
// output when it names none.
|
||||
// output when it names none or names the same file as that output, and
|
||||
// the command's own error output when it names the same file as that.
|
||||
// Those two are the streams the tool already has, so whatever they are
|
||||
// redirected to is written as the redirect says, never replaced.
|
||||
func through(
|
||||
cmd *cobra.Command, args []string,
|
||||
work func(io.Writer, io.Reader) error,
|
||||
@@ -157,11 +161,36 @@ func through(
|
||||
return fmt.Errorf("reading the output file: %w", err)
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
switch {
|
||||
case name == "", same(name, cmd.OutOrStdout()):
|
||||
return work(cmd.OutOrStdout(), src)
|
||||
case same(name, cmd.ErrOrStderr()):
|
||||
return work(cmd.ErrOrStderr(), src)
|
||||
default:
|
||||
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
|
||||
}
|
||||
|
||||
return output(name, src, work)
|
||||
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
|
||||
@@ -180,7 +209,78 @@ func input(cmd *cobra.Command, args []string) (io.Reader, func(), error) {
|
||||
return file, func() { _ = file.Close() }, nil
|
||||
}
|
||||
|
||||
// output has the work write a new file beside the named one, and puts
|
||||
// output has the work write to the named path, going by what is there
|
||||
// without following a final symlink:
|
||||
//
|
||||
// - nothing, or a regular file: replace writes a new file beside it
|
||||
// and renames that over it;
|
||||
// - a symlink: the same for what it points at, so that the link keeps
|
||||
// pointing where it did; one that points at nothing is refused;
|
||||
// - anything else, such as a named pipe or a device like /dev/null:
|
||||
// direct writes to it, since a rename would put a regular file in
|
||||
// its place.
|
||||
func output(
|
||||
name string, src io.Reader, work func(io.Writer, io.Reader) error,
|
||||
) error {
|
||||
info, err := os.Lstat(name)
|
||||
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return replace(name, src, work)
|
||||
case err != nil:
|
||||
return fmt.Errorf("looking at %s: %w", name, err)
|
||||
case info.Mode().IsRegular():
|
||||
return replace(name, src, work)
|
||||
case info.Mode().Type() == fs.ModeSymlink:
|
||||
// os.Stat follows the link as opening it would. /dev/fd/3
|
||||
// needs that: it reaches a pipe or a terminal through a link
|
||||
// that names no path.
|
||||
info, err = os.Stat(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("following %s: %w", name, err)
|
||||
}
|
||||
|
||||
if !info.Mode().IsRegular() {
|
||||
return direct(name, src, work)
|
||||
}
|
||||
|
||||
target, err := filepath.EvalSymlinks(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("following %s: %w", name, err)
|
||||
}
|
||||
|
||||
return replace(target, src, work)
|
||||
default:
|
||||
return direct(name, src, work)
|
||||
}
|
||||
}
|
||||
|
||||
// direct has the work write straight to the named path, which is there
|
||||
// and is not a regular file. No signal is caught, so one ends the tool
|
||||
// as it ends any other command.
|
||||
func direct(
|
||||
name string, src io.Reader, work func(io.Writer, io.Reader) error,
|
||||
) error {
|
||||
file, err := os.OpenFile(name, os.O_WRONLY, 0) //nolint:gosec // the -o path
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening %s: %w", name, err)
|
||||
}
|
||||
|
||||
failed := work(file, src)
|
||||
closeErr := file.Close()
|
||||
|
||||
if failed != nil {
|
||||
return failed
|
||||
}
|
||||
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("finishing %s: %w", name, closeErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// replace has the work write a new file beside the named one, and puts
|
||||
// the new file in the named file's place only when the work succeeded,
|
||||
// so a file that is already there survives a run that failed.
|
||||
//
|
||||
@@ -189,7 +289,7 @@ func input(cmd *cobra.Command, args []string) (io.Reader, func(), error) {
|
||||
// new file is removed and ErrInterrupted returned, at once if the work
|
||||
// is still running, without waiting for it, since it may be blocked
|
||||
// reading its input.
|
||||
func output(
|
||||
func replace(
|
||||
name string, src io.Reader, work func(io.Writer, io.Reader) error,
|
||||
) error {
|
||||
// received is registered before the context, so it gets every
|
||||
|
||||
Reference in New Issue
Block a user