// 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.

// Exploratory pentest for the head builtin.
//
// These tests probe integer edge cases, special files, long lines, resource
// exhaustion, path edge cases, and flag injection scenarios. Tests that might
// hang are run in a goroutine with time.After to bound execution.

package head_test

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

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

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

const pentestTimeout = 10 * time.Second

// headRun is a shorthand for runScript with AllowedPaths=dir.
func headRun(t *testing.T, script, dir string, extraPaths ...string) (stdout, stderr string, exitCode int) {
	t.Helper()
	paths := append([]string{dir}, extraPaths...)
	return runScript(t, script, dir, interp.AllowedPaths(paths))
}

// mustNotHang runs f in a goroutine and fails the test if it does not return
// within pentestTimeout.
func mustNotHang(t *testing.T, f func()) {
	t.Helper()
	done := make(chan struct{})
	go func() {
		defer close(done)
		f()
	}()
	select {
	case <-done:
	case <-time.After(pentestTimeout):
		t.Fatalf("operation did not complete within %s", pentestTimeout)
	}
}

// --- Integer edge cases: -n ---

func TestCmdPentestLinesNZero(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	stdout, _, code := headRun(t, "head -n 0 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCmdPentestLinesNOne(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("line1\nline2\n"), 0644))
	stdout, _, code := headRun(t, "head -n 1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "line1\n", stdout)
}

func TestCmdPentestLinesNMaxInt32(t *testing.T) {
	// 2147483647: must be clamped / handled without OOM on a small file.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -n 2147483647 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny\n", stdout)
	})
}

func TestCmdPentestLinesNMaxInt64(t *testing.T) {
	// 9223372036854775807 = max int64; clamped to maxHeadCount.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -n 9223372036854775807 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny\n", stdout)
	})
}

func TestCmdPentestLinesNMaxInt64PlusOne(t *testing.T) {
	// 9223372036854775808 overflows int64 — ParseInt returns an error.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	_, stderr, code := headRun(t, "head -n 9223372036854775808 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestLinesNHugeOverflow(t *testing.T) {
	// A numeric string too large for int64.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny\n"), 0644))
	_, stderr, code := headRun(t, "head -n 99999999999999999999 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestLinesNNegativeOne(t *testing.T) {
	// Negative counts must be rejected (we don't implement elide-tail mode).
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, "head -n -1 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestLinesNNegativeLarge(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, "head -n -9999999999 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestLinesNPlusZero(t *testing.T) {
	// "+0" is parsed by ParseInt as 0. GNU head -n +0 = no output.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	stdout, _, code := headRun(t, "head -n +0 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCmdPentestLinesNPlusOne(t *testing.T) {
	// "+1" = 1 line (positive sign, matches GNU head behavior).
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("alpha\nbeta\n"), 0644))
	stdout, _, code := headRun(t, "head -n +1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "alpha\n", stdout)
}

func TestCmdPentestLinesNEmptyString(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, `head -n "" f.txt`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestLinesNWhitespace(t *testing.T) {
	// A whitespace string — ParseInt rejects it.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, `head -n "   " f.txt`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

// --- Integer edge cases: -c ---

func TestCmdPentestBytesNZero(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello"), 0644))
	stdout, _, code := headRun(t, "head -c 0 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCmdPentestBytesNOne(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello"), 0644))
	stdout, _, code := headRun(t, "head -c 1 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "h", stdout)
}

func TestCmdPentestBytesNMaxInt32(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -c 2147483647 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny", stdout)
	})
}

func TestCmdPentestBytesNHugeOverflow(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny"), 0644))
	_, stderr, code := headRun(t, "head -c 99999999999999999999 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestBytesNNegative(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello"), 0644))
	_, stderr, code := headRun(t, "head -c -1 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestBytesPlusMaxInt64(t *testing.T) {
	// "+9223372036854775807" = max int64; clamped to maxHeadCount.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("tiny"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -c +9223372036854775807 f.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny", stdout)
	})
}

// --- Special files / infinite sources (Unix only) ---

func TestCmdPentestDevNull(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("no /dev/null on Windows")
	}
	// /dev/null is empty; head should produce no output and exit 0.
	dir := t.TempDir()
	stdout, _, code := headRun(t, "head /dev/null", dir, "/dev")
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout)
}

func TestCmdPentestDevZeroLineMode(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("no /dev/zero on Windows")
	}
	// head -n 3 /dev/zero: /dev/zero produces infinite NUL bytes with no \n.
	// The scanner's 1MiB line cap will trigger an error, so the command MUST
	// terminate rather than hang. Matches GNU behavior (scanner errors out).
	dir := t.TempDir()
	mustNotHang(t, func() {
		_, _, _ = headRun(t, "head -n 3 /dev/zero", dir, "/dev")
		// We only assert non-hang; exit code may be 0 or 1.
	})
}

func TestCmdPentestDevZeroByteMode(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("no /dev/zero on Windows")
	}
	// head -c 10 /dev/zero: must read exactly 10 bytes and stop.
	dir := t.TempDir()
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -c 10 /dev/zero", dir, "/dev")
		assert.Equal(t, 0, code)
		assert.Equal(t, string(bytes.Repeat([]byte{0}, 10)), stdout)
	})
}

// --- Long lines ---

func TestCmdPentestLongLineBelowCap(t *testing.T) {
	// 1MiB - 1 bytes of 'a' followed by \n — should succeed.
	dir := t.TempDir()
	content := bytes.Repeat([]byte("a"), 1<<20-1)
	content = append(content, '\n')
	require.NoError(t, os.WriteFile(filepath.Join(dir, "long.txt"), content, 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -n 1 long.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, string(content), stdout)
	})
}

func TestCmdPentestLongLineExactlyAtCap(t *testing.T) {
	// Exactly 1 MiB of 'a' with no newline. bufio.Scanner.Buffer(buf, max)
	// cannot hold a token of exactly max bytes (the limit is exclusive), so
	// this must error just like a line that exceeds the cap.
	dir := t.TempDir()
	content := bytes.Repeat([]byte("a"), 1<<20)
	require.NoError(t, os.WriteFile(filepath.Join(dir, "exact.txt"), content, 0644))
	mustNotHang(t, func() {
		_, stderr, code := headRun(t, "head -n 1 exact.txt", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "head:")
	})
}

func TestCmdPentestLongLineExactCapWithNewline(t *testing.T) {
	// Exactly 1 MiB of 'a' followed by \n. The scanner sees the token as
	// 1 MiB + 1 byte (content + newline). scanLinesPreservingNewline includes
	// the \n in the token, so the token exceeds maxHeadLineBytes and the
	// scanner must error.
	dir := t.TempDir()
	content := bytes.Repeat([]byte("a"), 1<<20)
	content = append(content, '\n')
	require.NoError(t, os.WriteFile(filepath.Join(dir, "exact_nl.txt"), content, 0644))
	mustNotHang(t, func() {
		_, stderr, code := headRun(t, "head -n 1 exact_nl.txt", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "head:")
	})
}

func TestCmdPentestLongLineExceedsCap(t *testing.T) {
	// 1MiB + 1 bytes of 'a' (no newline) — scanner errors (line too long).
	dir := t.TempDir()
	content := bytes.Repeat([]byte("a"), 1<<20+1)
	require.NoError(t, os.WriteFile(filepath.Join(dir, "huge.txt"), content, 0644))
	mustNotHang(t, func() {
		_, stderr, code := headRun(t, "head -n 1 huge.txt", dir)
		assert.Equal(t, 1, code)
		assert.Contains(t, stderr, "head:")
	})
}

func TestCmdPentestTwoLinesNearCap(t *testing.T) {
	// Two lines each just below the cap; head -n 2 should output both.
	dir := t.TempDir()
	line := bytes.Repeat([]byte("b"), 1<<20-2)
	line = append(line, '\n')
	content := append(append([]byte{}, line...), line...)
	require.NoError(t, os.WriteFile(filepath.Join(dir, "two.txt"), content, 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -n 2 two.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, string(content), stdout)
	})
}

// --- Memory / resource exhaustion ---

func TestCmdPentestMaxInt32CountSmallFile(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "small.txt"), []byte("small\n"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -n 2147483647 small.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "small\n", stdout)
	})
}

func TestCmdPentestMaxInt32ByteCountSmallFile(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "small.txt"), []byte("tiny"), 0644))
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, "head -c 2147483647 small.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "tiny", stdout)
	})
}

func TestCmdPentestManyFileArguments(t *testing.T) {
	// 210 file arguments: verify no file-descriptor leak or crash.
	dir := t.TempDir()
	var sb strings.Builder
	sb.WriteString("head -q -n 1")
	for i := 0; i < 210; i++ {
		name := fmt.Sprintf("f%03d.txt", i)
		require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(fmt.Sprintf("file%d\n", i)), 0644))
		sb.WriteString(" ")
		sb.WriteString(name)
	}
	mustNotHang(t, func() {
		stdout, _, code := headRun(t, sb.String(), dir)
		assert.Equal(t, 0, code)
		// 210 files × one "file%d\n" line = 210 lines.
		assert.Equal(t, 210, strings.Count(stdout, "\n"))
	})
}

func TestCmdPentestMillionLineFile(t *testing.T) {
	// 1M-line file: head -n 5 must output first 5 lines quickly.
	dir := t.TempDir()
	var buf bytes.Buffer
	for i := 0; i < 1_000_000; i++ {
		buf.WriteString("x\n")
	}
	require.NoError(t, os.WriteFile(filepath.Join(dir, "million.txt"), buf.Bytes(), 0644))
	done := make(chan struct{})
	go func() {
		defer close(done)
		stdout, _, code := headRun(t, "head -n 5 million.txt", dir)
		assert.Equal(t, 0, code)
		assert.Equal(t, "x\nx\nx\nx\nx\n", stdout)
	}()
	select {
	case <-done:
	case <-time.After(30 * time.Second):
		t.Fatal("head on 1M-line file did not complete within 30s")
	}
}

// --- Path and filename edge cases ---

func TestCmdPentestNonexistentFile(t *testing.T) {
	dir := t.TempDir()
	_, stderr, code := headRun(t, "head does_not_exist.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestEmptyFilename(t *testing.T) {
	// An empty string as a filename should produce an error.
	dir := t.TempDir()
	_, _, code := headRun(t, `head ""`, dir)
	assert.Equal(t, 1, code)
}

func TestCmdPentestFlagLikeName(t *testing.T) {
	// A file whose name starts with '-' must be accessible via '--'.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "-n"), []byte("flag-file\n"), 0644))
	stdout, _, code := headRun(t, "head -- -n", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "flag-file\n", stdout)
}

func TestCmdPentestDirectoryAsFile(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.Mkdir(filepath.Join(dir, "subdir"), 0755))
	_, stderr, code := headRun(t, "head subdir", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestOutsideSandbox(t *testing.T) {
	// Attempting to read a file outside the allowed path must be blocked.
	allowed := t.TempDir()
	secret := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(secret, "s.txt"), []byte("secret"), 0644))
	secretPath := filepath.ToSlash(filepath.Join(secret, "s.txt"))
	_, stderr, code := runScript(t, "head "+secretPath, allowed, interp.AllowedPaths([]string{allowed}))
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestPathTraversal(t *testing.T) {
	// Path traversal with ../ must be blocked by the sandbox.
	dir := t.TempDir()
	_, stderr, code := headRun(t, "head ../../etc/passwd", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

// --- Flag and argument injection ---

func TestCmdPentestUnknownLongFlag(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, "head --no-such-flag f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestUnknownShortFlag(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, "head -f f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestFollowFlagRejected(t *testing.T) {
	// --follow is a tail flag that must not be silently accepted by head.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, "head --follow f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestZeroTerminatedFlagRejected(t *testing.T) {
	// -z / --zero-terminated is a GNU extension we do not support.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, "head -z f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

func TestCmdPentestMultipleDashes(t *testing.T) {
	// Two '-' args with seekable stdin (redirected from a file).
	// readLines wraps the scanner in a byteCountReader and seeks back any
	// bytes the scanner buffered but did not emit. This allows the second '-'
	// to read from the correct stream position, matching GNU head behaviour.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "src.txt"), []byte("alpha\nbeta\ngamma\n"), 0644))
	stdout, _, code := headRun(t, "head -q -n 1 - - < src.txt", dir)
	assert.Equal(t, 0, code)
	// First '-' outputs "alpha\n"; second '-' reads from the seeked-back position → "beta\n".
	assert.Equal(t, "alpha\nbeta\n", stdout)
}

func TestCmdPentestFlagViaExpansion(t *testing.T) {
	// Flag injected via variable expansion: unknown flag → exit 1.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("hello\n"), 0644))
	_, stderr, code := headRun(t, `flag="-f"; head $flag f.txt`, dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

// --- Behavior matching (GNU comparison notes) ---

func TestCmdPentestGNUMatchPositivePrefix(t *testing.T) {
	// GNU head -n +N treats + as a positive sign, outputting N lines.
	// Matches GNU: ghead -n +2 = first 2 lines.
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("alpha\nbeta\ngamma\n"), 0644))
	stdout, _, code := headRun(t, "head -n +2 f.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "alpha\nbeta\n", stdout) // Matches GNU
}

func TestCmdPentestGNUMatchNegativeRejected(t *testing.T) {
	// GNU head -n -N is elide-tail mode (all but last N lines).
	// We intentionally do NOT support this (safer-than-GNU: we reject
	// rather than implement a ring buffer).
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("alpha\nbeta\ngamma\n"), 0644))
	_, stderr, code := headRun(t, "head -n -1 f.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

// --- GTFOBins validation ---
//
// GTFOBins documents head as capable of reading arbitrary files via
// "head -c-0 /path/to/file" (negative byte count = read all bytes).
// In rshell, negative counts are rejected by parseCount.
// See: https://gtfobins.org/gtfobins/head/

// TestHeadGTFOBinsFileReadNegativeBytes verifies that the GTFOBins file-read
// technique "head -c-0" does NOT expose file contents.
//
// GTFOBins: https://gtfobins.org/gtfobins/head/
// Technique: head -c-0 /path/to/input-file
//
// In GNU head, -c-0 means "all bytes except the last 0" = entire file.
// In rshell, pflag parses -c-0 as -c with value "-0". strconv.ParseInt
// treats "-0" as 0 (not negative), so this outputs 0 bytes — safely
// producing no output. The attack is neutralized because our parseCount
// does not implement GNU's negative-count "elide from tail" mode.
func TestHeadGTFOBinsFileReadNegativeBytes(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "secret.txt"), []byte("secret data\n"), 0644))
	// Attempt the exact GTFOBins technique: head -c-0
	// Result: 0 bytes output (safe — file contents are NOT exposed)
	stdout, _, code := headRun(t, "head -c-0 secret.txt", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "", stdout, "head -c-0 must not expose file contents")
}

// TestHeadGTFOBinsFileReadNegativeNonZero verifies that head -c-N for
// N>0 is also safe — negative counts are rejected by parseCount.
//
// GTFOBins: https://gtfobins.org/gtfobins/head/
// Technique: head -c-1 /path/to/input-file
func TestHeadGTFOBinsFileReadNegativeNonZero(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "secret.txt"), []byte("secret data\n"), 0644))
	_, stderr, code := headRun(t, "head -c-1 secret.txt", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}

// TestHeadGTFOBinsFileReadSandboxEscape verifies that even with valid flags,
// head cannot read files outside the AllowedPaths sandbox.
//
// GTFOBins: https://gtfobins.org/gtfobins/head/
// Technique: head /path/to/input-file
func TestHeadGTFOBinsFileReadSandboxEscape(t *testing.T) {
	allowed := t.TempDir()
	secret := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data"), 0644))
	secretPath := filepath.ToSlash(filepath.Join(secret, "secret.txt"))
	_, stderr, code := runScript(t, "head "+secretPath, allowed, interp.AllowedPaths([]string{allowed}))
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "head:")
}
