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

// Pentest test cases for the xargs builtin. These exercise hostile and
// boundary inputs against the integration surface (full shell pipeline)
// to demonstrate that xargs cannot be used to escape the sandbox or cause
// resource exhaustion. Each test is bounded by a per-test deadline so a
// regression that hangs is caught quickly.
//
// Run with:
//
//	go test ./builtins/xargs/ -run TestXargsPentest -timeout 120s
package xargs_test

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

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

// --- Integer edge cases ---

func TestXargsPentestNRangeEdges(t *testing.T) {
	dir := t.TempDir()
	cases := []struct {
		flag       string
		val        string
		shouldFail bool
	}{
		{"-n", "0", true},
		{"-n", "1", false},
		{"-n", "2147483647", true}, // > HardMaxArgs
		{"-n", "9223372036854775807", true},
		{"-n", "9999999999999999999", true},
		{"-n", "-1", true},
		{"-n", "-9999999999", true},
		{"-n", "", true},
		{"-n", "   ", true},
		{"-c", "1", true}, // unknown flag rejection
	}
	for _, tc := range cases {
		t.Run(tc.flag+"="+tc.val, func(t *testing.T) {
			cmd := fmt.Sprintf("echo a | xargs %s %q echo", tc.flag, tc.val)
			_, _, code := cmdRun(t, cmd, dir)
			if tc.shouldFail {
				assert.NotEqual(t, 0, code)
			} else {
				assert.Equal(t, 0, code)
			}
		})
	}
}

func TestXargsPentestSRangeEdges(t *testing.T) {
	dir := t.TempDir()
	cases := []struct {
		val        string
		shouldFail bool
	}{
		{"0", true},
		{"-1", true},
		{"1", true},                   // -s 1 cannot fit `echo` itself, GNU xargs exits 1
		{"9999999999999999999", true}, // out of range
		{maxInt32String, false},       // clamped to HardMaxChars, plenty of room for "echo a"
	}
	for _, tc := range cases {
		t.Run("-s="+tc.val, func(t *testing.T) {
			cmd := fmt.Sprintf("echo a | xargs -s %q echo", tc.val)
			_, _, code := cmdRun(t, cmd, dir)
			if tc.shouldFail {
				assert.NotEqual(t, 0, code)
			} else {
				assert.Equal(t, 0, code)
			}
		})
	}
}

const maxInt32String = "2147483647"

// --- Path traversal & sandbox ---

func TestXargsPentestArgFileTraversal(t *testing.T) {
	allowed := t.TempDir()
	parent := filepath.Dir(allowed)
	require.NoError(t, os.WriteFile(filepath.Join(parent, "secret.txt"),
		[]byte("nope\n"), 0644))
	defer os.Remove(filepath.Join(parent, "secret.txt"))

	for _, path := range []string{
		"../secret.txt",
		"../../secret.txt",
		"./../secret.txt",
		"//etc/hosts",
	} {
		t.Run(path, func(t *testing.T) {
			_, stderr, code := runScript(t, "xargs -a "+path+" echo", allowed,
				interp.AllowedPaths([]string{allowed}))
			assert.Equal(t, 1, code)
			assert.NotEmpty(t, stderr)
		})
	}
}

func TestXargsPentestArgFileEmpty(t *testing.T) {
	dir := t.TempDir()
	// pflag: an empty value is still a value; our buildOptions catches it.
	_, stderr, code := cmdRun(t, "xargs -a '' echo", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "xargs:")
}

// --- GTFOBins shell escape ---

func TestXargsPentestGTFOBinsShellEscape(t *testing.T) {
	dir := t.TempDir()
	// /bin/sh is not a registered builtin; AllowedCommands is set to only
	// xargs+echo. xargs must refuse the call.
	writeFile(t, dir, "in.txt", "")
	_, stderr, code := runScript(t, "xargs -a in.txt /bin/sh", dir,
		interp.AllowedPaths([]string{dir}),
		interp.AllowedCommands([]string{"rshell:xargs", "rshell:echo"}))
	assert.NotEqual(t, 0, code)
	assert.True(t,
		strings.Contains(stderr, "command not allowed") ||
			strings.Contains(stderr, "unknown command"),
		"stderr should mention rejection, got %q", stderr)
}

func TestXargsPentestGTFOBinsFileRead(t *testing.T) {
	// `xargs -a /path -0` lets you read a file by passing it as args to
	// echo. The path-read primitive is bounded by AllowedPaths (sandbox).
	dir := t.TempDir()
	writeFile(t, dir, "secret.txt", "S3CR3T\n")
	stdout, _, code := cmdRun(t, "xargs -0 -a secret.txt echo", dir)
	assert.Equal(t, 0, code)
	// Even though the technique works for in-sandbox files, it is no
	// stronger than `cat`. The point is that reads outside AllowedPaths
	// are blocked, which the traversal test above covers.
	assert.Contains(t, stdout, "S3CR3T")
}

// --- Long-line and oversize-token DoS ---

func TestXargsPentestTokenJustUnderCap(t *testing.T) {
	dir := t.TempDir()
	// A token of (MaxTokenBytes - 1) bytes is accepted by the tokenizer.
	// We pick -s large enough that the cmd name + token + NUL overhead
	// fits within the size budget (cmd "echo" + 1 + token + 1).
	body := make([]byte, (1<<20)-1)
	for i := range body {
		body[i] = 'a'
	}
	body = append(body, '\n')
	require.NoError(t, os.WriteFile(filepath.Join(dir, "edge.txt"), body, 0644))
	// HardMaxChars clamps -s to 1 MiB, but the resolved line ("echo " +
	// token + NUL) is 1 MiB + 5, exceeding the cap. GNU xargs exits 1
	// for this case ("argument line too long"); rshell now matches.
	_, _, code := cmdRun(t, "xargs -a edge.txt echo > /dev/null", dir)
	assert.Equal(t, 1, code)
}

func TestXargsPentestTokenExactlyCap(t *testing.T) {
	dir := t.TempDir()
	// A token of exactly MaxTokenBytes (1 MiB) is also accepted by the
	// tokenizer; like the under-cap case, the resolved command line
	// exceeds HardMaxChars so xargs exits 1 — matching GNU.
	body := make([]byte, 1<<20)
	for i := range body {
		body[i] = 'a'
	}
	body = append(body, '\n')
	require.NoError(t, os.WriteFile(filepath.Join(dir, "edge.txt"), body, 0644))
	_, _, code := cmdRun(t, "xargs -a edge.txt echo > /dev/null", dir)
	assert.Equal(t, 1, code)
}

func TestXargsPentestTokenOverCap(t *testing.T) {
	dir := t.TempDir()
	body := make([]byte, (1<<20)+10)
	for i := range body {
		body[i] = 'a'
	}
	require.NoError(t, os.WriteFile(filepath.Join(dir, "huge.txt"), body, 0644))
	_, stderr, code := cmdRun(t, "xargs -a huge.txt echo > /dev/null", dir)
	assert.NotEqual(t, 0, code)
	assert.Contains(t, stderr, "xargs:")
}

// --- Many small tokens (file descriptor / memory pressure) ---

func TestXargsPentestManyArgs(t *testing.T) {
	dir := t.TempDir()
	var sb strings.Builder
	for i := 0; i < 5000; i++ {
		fmt.Fprintf(&sb, "tok%05d ", i)
	}
	require.NoError(t, os.WriteFile(filepath.Join(dir, "many.txt"),
		[]byte(sb.String()), 0644))
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	stdout, _, code := runScriptCtx(ctx, t, "xargs -a many.txt -n 10 echo | wc -l", dir,
		interp.AllowedPaths([]string{dir}))
	assert.Equal(t, 0, code)
	assert.NotEmpty(t, stdout)
}

// --- Filename edge cases via -a ---

func TestXargsPentestArgFileSpecialNames(t *testing.T) {
	dir := t.TempDir()
	// Filenames with whitespace, unicode, and leading dash.
	cases := []string{
		"file with spaces.txt",
		"-file.txt",
		"unicode_τεστ.txt",
	}
	for _, name := range cases {
		t.Run(name, func(t *testing.T) {
			require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte("ok\n"), 0644))
			defer os.Remove(filepath.Join(dir, name))
			// Using -a -- "name" so leading dash is treated as a path.
			stdout, _, code := cmdRun(t, "xargs -a "+shellQuote(name)+" echo", dir)
			assert.Equal(t, 0, code)
			assert.Contains(t, stdout, "ok")
		})
	}
}

func shellQuote(s string) string {
	return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}

// --- Flag injection via word expansion ---

func TestXargsPentestFlagViaExpansion(t *testing.T) {
	dir := t.TempDir()
	// A variable that expands to a flag-like value should still be
	// recognised by pflag (POSIX behaviour). We check that xargs rejects
	// bogus flags consistently regardless of how they appear on the
	// command line.
	_, stderr, code := cmdRun(t, "f=--no-such-flag; xargs $f echo", dir)
	assert.Equal(t, 1, code)
	assert.NotEmpty(t, stderr)
}

func TestXargsPentestEndOfFlagsSeparator(t *testing.T) {
	dir := t.TempDir()
	// `--` ends flag parsing; what follows is the command name and args.
	stdout, _, code := cmdRun(t, "echo a | xargs -- echo HEAD", dir)
	assert.Equal(t, 0, code)
	assert.Equal(t, "HEAD a\n", stdout)
}

func TestXargsPentestStdinDashIsLiteral(t *testing.T) {
	dir := t.TempDir()
	// xargs has no special handling for `-`, unlike cat; passing `-` as
	// the command means we try to invoke a builtin literally named "-",
	// which doesn't exist and is rejected.
	_, stderr, code := cmdRun(t, "echo x | xargs -", dir)
	assert.NotEqual(t, 0, code)
	assert.NotEmpty(t, stderr)
}

// --- Sub-command exit code propagation ---

func TestXargsPentestSubCmdFailureExit123(t *testing.T) {
	dir := t.TempDir()
	_, _, code := cmdRun(t, "echo a | xargs false", dir)
	assert.Equal(t, 123, code)
}

func TestXargsPentestSubCmdFailureContinues(t *testing.T) {
	dir := t.TempDir()
	// With -n 1 each item runs once. `false` always fails; xargs must run
	// it for every item and return 123 (any failure ≥ 1).
	_, _, code := cmdRun(t, "echo 'a b c' | xargs -n 1 false", dir)
	assert.Equal(t, 123, code)
}

// --- Cancellation (ensure pre-cancelled context returns promptly) ---

func TestXargsPentestPreCancelled(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "in.txt"),
		[]byte(strings.Repeat("a ", 5000)), 0644))
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	done := make(chan struct{})
	go func() {
		runScriptCtx(ctx, t, "xargs -a in.txt echo > /dev/null", dir,
			interp.AllowedPaths([]string{dir}))
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("pre-cancelled context not respected")
	}
}

// --- Very short -s budgets ---

func TestXargsPentestSExtremelySmall(t *testing.T) {
	dir := t.TempDir()
	// -s 1 cannot fit even the command name. With -x, abort. Without -x,
	// best-effort with a warning.
	_, stderr, code := cmdRun(t, "echo 'a b c' | xargs -s 1 -x echo", dir)
	assert.Equal(t, 1, code)
	assert.Contains(t, stderr, "xargs:")
}

// --- Replace-string with metacharacters ---

func TestXargsPentestReplaceShellMeta(t *testing.T) {
	dir := t.TempDir()
	// REPLSTR substitution is purely textual; shell metacharacters in
	// item content are forwarded verbatim to the sub-command and never
	// re-interpreted by a shell (we don't have one in the loop).
	stdout, _, code := cmdRun(t, "printf 'a$IFS;rm -rf /\\nb\\n' | xargs -I {} echo {}", dir)
	assert.Equal(t, 0, code)
	// Both lines come through as plain text.
	assert.Equal(t, "a$IFS;rm -rf /\nb\n", stdout)
}
