From 29552e92a4029aa77f10983f355e2ac3cd2cdec6 Mon Sep 17 00:00:00 2001 From: Deepak Bhagat Date: Tue, 18 Aug 2026 02:48:20 +0530 Subject: [PATCH] butane: use friendly filename in stdin read error The stdin read error used infile.Name(), which is "/dev/stdin" on Linux, instead of the already-computed friendly filename (""). Refactor input reading into readInput() and report the friendly name on read failures. Fixes coreos/ignition#2281 Addresses coreos/butane#726 Signed-off-by: Deepak Bhagat --- butane/internal/main.go | 36 ++++++---- butane/internal/main_test.go | 128 +++++++++++++++++++++++++++++++++++ docs/release-notes.md | 2 + 3 files changed, 152 insertions(+), 14 deletions(-) create mode 100644 butane/internal/main_test.go diff --git a/butane/internal/main.go b/butane/internal/main.go index 1d384ff37..324f0a842 100644 --- a/butane/internal/main.go +++ b/butane/internal/main.go @@ -40,6 +40,26 @@ func isCharDevice(f *os.File) bool { return stat.Mode()&os.ModeCharDevice != 0 } +func readInput(input string) ([]byte, string, error) { + infile := os.Stdin + filename := "" + if input != "" { + f, err := os.Open(input) + if err != nil { + return nil, input, fmt.Errorf("failed to open %s: %w", input, err) + } + defer func() { _ = f.Close() }() + infile = f + filename = input + } + + data, err := io.ReadAll(infile) + if err != nil { + return nil, filename, fmt.Errorf("failed to read %s: %w", filename, err) + } + return data, filename, nil +} + func main() { var ( input string @@ -110,21 +130,9 @@ func main() { os.Exit(0) } - infile := os.Stdin - filename := "" - if input != "" { - var err error - infile, err = os.Open(input) - if err != nil { - fail("failed to open %s: %v\n", input, err) - } - defer func() { _ = infile.Close() }() - filename = input - } - - dataIn, err := io.ReadAll(infile) + dataIn, filename, err := readInput(input) if err != nil { - fail("failed to read %s: %v\n", infile.Name(), err) + fail("%v\n", err) } dataOut, r, err := config.TranslateBytes(dataIn, options) diff --git a/butane/internal/main_test.go b/butane/internal/main_test.go new file mode 100644 index 000000000..1d0d9d9b6 --- /dev/null +++ b/butane/internal/main_test.go @@ -0,0 +1,128 @@ +// Copyright 2019 Red Hat, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestReadInput(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T) (input string, cleanup func()) + wantData []byte + wantErr bool + }{ + { + name: "stdin", + setup: func(t *testing.T) (string, func()) { + content := []byte("hello from stdin") + orig := os.Stdin + tmp, err := os.CreateTemp("", "butane-stdin") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + if _, err := tmp.Write(content); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + if _, err := tmp.Seek(0, 0); err != nil { + t.Fatalf("failed to seek temp file: %v", err) + } + os.Stdin = tmp + return "", func() { + os.Stdin = orig + tmp.Close() + os.Remove(tmp.Name()) + } + }, + wantData: []byte("hello from stdin"), + wantErr: false, + }, + { + name: "empty stdin", + setup: func(t *testing.T) (string, func()) { + orig := os.Stdin + tmp, err := os.CreateTemp("", "butane-stdin-empty") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + if _, err := tmp.Seek(0, 0); err != nil { + t.Fatalf("failed to seek temp file: %v", err) + } + os.Stdin = tmp + return "", func() { + os.Stdin = orig + tmp.Close() + os.Remove(tmp.Name()) + } + }, + wantData: []byte{}, + wantErr: false, + }, + { + name: "file", + setup: func(t *testing.T) (string, func()) { + dir := t.TempDir() + path := filepath.Join(dir, "input.bu") + content := []byte("variant: fcos") + if err := os.WriteFile(path, content, 0644); err != nil { + t.Fatalf("failed to write file: %v", err) + } + return path, func() {} + }, + wantData: []byte("variant: fcos"), + wantErr: false, + }, + { + name: "missing file", + setup: func(t *testing.T) (string, func()) { + missing := filepath.Join(t.TempDir(), "does-not-exist") + return missing, func() {} + }, + wantData: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input, cleanup := tt.setup(t) + defer cleanup() + + data, filename, err := readInput(input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if string(data) != string(tt.wantData) { + t.Errorf("expected data %q, got %q", tt.wantData, data) + } + + wantName := "" + if input != "" { + wantName = input + } + if filename != wantName { + t.Errorf("expected filename %q, got %q", wantName, filename) + } + }) + } +} diff --git a/docs/release-notes.md b/docs/release-notes.md index b9020a1e8..f0e29de3d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,8 @@ nav_order: 9 ### Bug fixes +- butane: report the friendly input name (`` instead of `/dev/stdin`) in stdin read errors + ## Upcoming Ignition 2.27.0 (unreleased)