Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions builtins/ls/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc {
if opts.longFmt && info.Mode()&iofs.ModeSymlink != 0 {
if t, err := callCtx.ReadlinkFile(ctx, p); err == nil {
pa.linkTarget = t
} else {
// GNU ls still lists the entry, without " -> target".
callCtx.Errf("ls: cannot read symbolic link '%s': %s\n", builtins.SafeOperand(p), callCtx.PortableErr(err))
failed = true
}
// Stat the target for -F/-p indicators (nil for dangling links).
if ti, err := callCtx.StatFile(ctx, p); err == nil {
Expand Down Expand Up @@ -397,6 +401,10 @@ func listDir(ctx context.Context, callCtx *builtins.CallContext, dir string, opt
fullPath := joinPath(dir, name)
if t, err := callCtx.ReadlinkFile(ctx, fullPath); err == nil {
ei.linkTarget = t
} else {
// GNU ls still lists the entry, without " -> target".
callCtx.Errf("ls: cannot read symbolic link '%s': %s\n", builtins.SafeOperand(fullPath), callCtx.PortableErr(err))
failed = true
}
// Stat the target for -F/-p indicators (nil for dangling links).
if ti, err := callCtx.StatFile(ctx, fullPath); err == nil {
Expand Down
81 changes: 81 additions & 0 deletions builtins/ls/readlink_error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

package ls

import (
"bytes"
"context"
"errors"
iofs "io/fs"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/DataDog/rshell/builtins"
)

// runLsWithReadlinkFault runs ls with ReadlinkFile failing with EIO-like
// errors. A symlink target cannot fail to read natively, so the failure has to
// be injected through the CallContext.
func runLsWithReadlinkFault(t *testing.T, args ...string) (stdout, stderr string, code uint8) {
t.Helper()
var out, errOut bytes.Buffer
callCtx := &builtins.CallContext{
Stdout: &out,
Stderr: &errOut,
Now: time.Now(),
PortableErr: func(err error) string { return err.Error() },
LstatFile: func(_ context.Context, p string) (iofs.FileInfo, error) { return os.Lstat(p) },
StatFile: func(_ context.Context, p string) (iofs.FileInfo, error) { return os.Stat(p) },
ReadDir: func(_ context.Context, p string) ([]iofs.DirEntry, error) { return os.ReadDir(p) },
ReadlinkFile: func(_ context.Context, p string) (string, error) {
return "", &os.PathError{Op: "readlink", Path: p, Err: errors.New("input/output error")}
},
}
fs := pflag.NewFlagSet("ls", pflag.ContinueOnError)
handler := registerFlags(fs)
require.NoError(t, fs.Parse(args))
res := handler(context.Background(), callCtx, fs.Args())
return out.String(), errOut.String(), res.Code
}

func TestLsLongReadlinkFailureInDirectory(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "target"), nil, 0o644))
if err := os.Symlink("target", filepath.Join(dir, "link")); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}

stdout, stderr, code := runLsWithReadlinkFault(t, "-l", dir)
assert.Equal(t, uint8(1), code)
assert.Contains(t, stderr, "ls: cannot read symbolic link '"+builtins.SafeOperand(joinPath(dir, "link"))+"': ")
assert.Contains(t, stderr, "input/output error")
// The entry is still listed, without its target.
assert.Contains(t, stdout, " link\n")
assert.NotContains(t, stdout, "->")
}

func TestLsLongReadlinkFailureOnOperand(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "target"), nil, 0o644))
link := filepath.Join(dir, "link")
if err := os.Symlink("target", link); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}

stdout, stderr, code := runLsWithReadlinkFault(t, "-l", link)
assert.Equal(t, uint8(1), code)
assert.Contains(t, stderr, "ls: cannot read symbolic link '"+builtins.SafeOperand(link)+"': ")
assert.Contains(t, stderr, "input/output error")
assert.True(t, strings.HasSuffix(strings.TrimRight(stdout, "\n"), " "+link), "entry not listed: %q", stdout)
assert.NotContains(t, stdout, "->")
}
Loading