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 wrote a new file beside the named path and renamed it over that path, which replaced a symlink, a named pipe or a device such as /dev/null with a regular file and made -o /dev/stdout fail. The path is now looked at without following a final symlink: a missing path or a regular file is replaced by rename as before, a symlink gets the same treatment for what it points at, and anything else is written to directly, without catching signals. A symlink that points at nothing is refused. The README says so, and that a replaced file has mode 0600. Model: opus-5-5
This commit is contained in:
+74
-2
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -180,7 +181,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/stdout
|
||||
// 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 +261,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