Skip to content
Merged
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
16 changes: 16 additions & 0 deletions allowedpaths/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,22 @@ func (s *Sandbox) Lstat(path string, cwd string) (fs.FileInfo, error) {
return info, nil
}

// Readlink returns the destination of a symbolic link within the sandbox.
func (s *Sandbox) Readlink(path string, cwd string) (string, error) {
absPath := toAbs(path, cwd)

root, relPath, ok := s.resolve(absPath)
if !ok {
return "", &os.PathError{Op: "readlink", Path: path, Err: os.ErrPermission}
}

target, err := root.Readlink(relPath)
if err != nil {
return "", PortablePathError(err)
}
return target, nil
}

// Close releases all os.Root file descriptors. It is safe to call multiple times.
func (s *Sandbox) Close() error {
if s == nil {
Expand Down
4 changes: 4 additions & 0 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ type CallContext struct {
// LstatFile returns file info within the shell's path restrictions (does not follow symlinks).
LstatFile func(ctx context.Context, path string) (fs.FileInfo, error)

// ReadlinkFile returns the destination of a symbolic link within the
// shell's path restrictions.
ReadlinkFile func(ctx context.Context, path string) (string, error)

// AccessFile checks whether the file at path is accessible with the given mode
// within the shell's path restrictions. Mode: 0x04=read, 0x02=write, 0x01=execute.
AccessFile func(ctx context.Context, path string, mode uint32) error
Expand Down
81 changes: 60 additions & 21 deletions builtins/ls/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,17 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc {
}
}
if !isDir || opts.dirOnly {
files = append(files, pathArg{name: p, info: info})
pa := pathArg{name: p, info: info}
if opts.longFmt && info.Mode()&iofs.ModeSymlink != 0 {
if t, err := callCtx.ReadlinkFile(ctx, p); err == nil {
pa.linkTarget = t
}
// Stat the target for -F/-p indicators (nil for dangling links).
if ti, err := callCtx.StatFile(ctx, p); err == nil {
pa.linkTargetInfo = ti
}
}
files = append(files, pa)
} else {
dirs = append(dirs, pathArg{name: p, info: info})
}
Expand All @@ -251,7 +261,7 @@ func registerFlags(fs *builtins.FlagSet) builtins.HandlerFunc {
cw = computeColWidths(files, func(a pathArg) iofs.FileInfo { return a.info }, func(a pathArg) (string, string, string) { return a.owner, a.group, a.nlink }, opts)
}
for _, f := range files {
printEntry(callCtx, f.name, f.info, f.owner, f.group, f.nlink, opts, now, cw)
printEntry(callCtx, f.name, f.info, f.owner, f.group, f.nlink, f.linkTarget, f.linkTargetInfo, opts, now, cw)
}
}

Expand Down Expand Up @@ -297,11 +307,13 @@ type options struct {
}

type pathArg struct {
name string
info iofs.FileInfo
owner string // cached by fileOwner (populated when longFmt)
group string
nlink string
name string
info iofs.FileInfo
owner string // cached by fileOwner (populated when longFmt)
group string
nlink string
linkTarget string // symlink destination (populated when longFmt + symlink)
linkTargetInfo iofs.FileInfo // target's FileInfo for indicator (may be nil for dangling links)
}

func listDir(ctx context.Context, callCtx *builtins.CallContext, dir string, opts *options, depth int, now time.Time) error {
Expand Down Expand Up @@ -347,12 +359,14 @@ func listDir(ctx context.Context, callCtx *builtins.CallContext, dir string, opt

// Get FileInfo for sorting (if needed) and for long format.
type entryInfo struct {
name string
info iofs.FileInfo
isSymlink bool
owner string // cached by fileOwner (populated when longFmt)
group string
nlink string
name string
info iofs.FileInfo
isSymlink bool
owner string // cached by fileOwner (populated when longFmt)
group string
nlink string
linkTarget string // symlink destination (populated when longFmt + symlink)
linkTargetInfo iofs.FileInfo // target's FileInfo for indicator (may be nil for dangling links)
}

failed := false
Expand All @@ -372,11 +386,24 @@ func listDir(ctx context.Context, callCtx *builtins.CallContext, dir string, opt
failed = true
continue
}
infoEntries = append(infoEntries, entryInfo{
name: name,
info: info,
isSymlink: e.Type()&iofs.ModeSymlink != 0,
})
ei := entryInfo{
name: name,
info: info,
// Check both Type() and info.Mode() — Type() can be 0 on
// filesystems that report DT_UNKNOWN.
isSymlink: e.Type()&iofs.ModeSymlink != 0 || info.Mode()&iofs.ModeSymlink != 0,
}
if opts.longFmt && ei.isSymlink {
fullPath := joinPath(dir, name)
if t, err := callCtx.ReadlinkFile(ctx, fullPath); err == nil {
ei.linkTarget = t
}
// Stat the target for -F/-p indicators (nil for dangling links).
if ti, err := callCtx.StatFile(ctx, fullPath); err == nil {
ei.linkTargetInfo = ti
}
}
infoEntries = append(infoEntries, ei)
}

// Synthesize . and .. for -a (os.ReadDir never includes them).
Expand Down Expand Up @@ -432,7 +459,7 @@ func listDir(ctx context.Context, callCtx *builtins.CallContext, dir string, opt
if ctx.Err() != nil {
break
}
printEntry(callCtx, ei.name, ei.info, ei.owner, ei.group, ei.nlink, opts, now, cw)
printEntry(callCtx, ei.name, ei.info, ei.owner, ei.group, ei.nlink, ei.linkTarget, ei.linkTargetInfo, opts, now, cw)
}

// Only warn on implicit truncation (no explicit --offset/--limit).
Expand Down Expand Up @@ -525,7 +552,7 @@ func computeColWidths[T any](entries []T, getInfo func(T) iofs.FileInfo, getOwne
return w
}

func printEntry(callCtx *builtins.CallContext, name string, info iofs.FileInfo, owner, group, nlink string, opts *options, now time.Time, cw colWidths) {
func printEntry(callCtx *builtins.CallContext, name string, info iofs.FileInfo, owner, group, nlink, linkTarget string, linkTargetInfo iofs.FileInfo, opts *options, now time.Time, cw colWidths) {
if opts.longFmt {
mode := formatMode(info)
size := info.Size()
Expand All @@ -538,12 +565,24 @@ func printEntry(callCtx *builtins.CallContext, name string, info iofs.FileInfo,
sizeStr = fmt.Sprintf("%d", size)
}

suffix := indicator(info, opts)
if linkTarget != "" {
targetIndicator := ""
// GNU ls only applies -F (classify) indicators to symlink
// targets, not -p (append-slash-only). For example,
// ls -lF shows "link -> dir/" but ls -lp shows "link -> dir".
if linkTargetInfo != nil && opts.classify {
targetIndicator = indicator(linkTargetInfo, opts)
}
suffix = " -> " + linkTarget + targetIndicator
Comment thread
matt-dz marked this conversation as resolved.
}
Comment thread
matt-dz marked this conversation as resolved.

timeStr := formatTime(modTime, now)
callCtx.Outf("%s %*s %-*s %-*s %*s %s %s%s\n",
mode, cw.nlink, nlink,
cw.owner, owner, cw.group, group,
cw.size, sizeStr, timeStr,
name, indicator(info, opts))
name, suffix)
} else {
callCtx.Outf("%s%s\n", name, indicator(info, opts))
}
Expand Down
6 changes: 6 additions & 0 deletions interp/runner_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,9 @@ func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) {
LstatFile: func(ctx context.Context, path string) (fs.FileInfo, error) {
return r.sandbox.Lstat(path, dir)
},
ReadlinkFile: func(ctx context.Context, path string) (string, error) {
return r.sandbox.Readlink(path, dir)
},
AccessFile: func(ctx context.Context, path string, mode uint32) error {
return r.sandbox.Access(path, dir, mode)
},
Expand Down Expand Up @@ -363,6 +366,9 @@ func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) {
LstatFile: func(ctx context.Context, path string) (fs.FileInfo, error) {
return r.sandbox.Lstat(path, HandlerCtx(r.handlerCtx(ctx, todoPos)).Dir)
},
ReadlinkFile: func(ctx context.Context, path string) (string, error) {
return r.sandbox.Readlink(path, HandlerCtx(r.handlerCtx(ctx, todoPos)).Dir)
},
AccessFile: func(ctx context.Context, path string, mode uint32) error {
return r.sandbox.Access(path, HandlerCtx(r.handlerCtx(ctx, todoPos)).Dir, mode)
},
Expand Down
16 changes: 16 additions & 0 deletions tests/scenarios/cmd/ls/long_format/symlink_append_slash.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
description: ls -lp does not append slash to symlink targets (only -F does)
setup:
files:
- path: dir/subdir/.keep
content: ""
- path: dir/dirlink
symlink: subdir
input:
allowed_paths: ["$DIR"]
script: |+
ls -lp dir/
expect:
stdout_contains:
- "dirlink -> subdir"
- "subdir/"
exit_code: 0
22 changes: 22 additions & 0 deletions tests/scenarios/cmd/ls/long_format/symlink_classify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
description: ls -lF shows classification indicators on symlink targets
setup:
files:
- path: dir/file.txt
content: "hello"
chmod: 0644
- path: dir/subdir/.keep
content: ""
- path: dir/dirlink
symlink: subdir
- path: dir/filelink
symlink: file.txt
input:
allowed_paths: ["$DIR"]
script: |+
ls -lF dir/
expect:
stdout_contains:
- "dirlink -> subdir/"
- "filelink -> file.txt"
stderr: ""
exit_code: 0
17 changes: 17 additions & 0 deletions tests/scenarios/cmd/ls/long_format/symlink_target.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
description: ls -l shows symlink target with -> arrow
setup:
files:
- path: file.txt
content: "hello"
chmod: 0644
- path: link.txt
symlink: file.txt
input:
allowed_paths: ["$DIR"]
script: |+
ls -l link.txt
expect:
stdout_contains:
- "link.txt -> file.txt"
stderr: ""
exit_code: 0
18 changes: 18 additions & 0 deletions tests/scenarios/cmd/ls/long_format/symlink_target_in_dir.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
description: ls -l in a directory shows symlink targets with -> arrow
setup:
files:
- path: dir/file.txt
content: "hello"
chmod: 0644
- path: dir/link.txt
symlink: file.txt
input:
allowed_paths: ["$DIR"]
script: |+
ls -l dir/
expect:
stdout_contains:
- "link.txt -> file.txt"
- "file.txt"
stderr: ""
exit_code: 0
Loading