//go:build gorules

package rules

import (
	"strings"

	"github.com/quasilyte/go-ruleguard/dsl"
)

// nolint:unused
func resourceCleanup(m dsl.Matcher) {
	// this rule defends against use of internal.CloseAndLogError() without a defer statement
	m.Match(`$res, $err := $resolver.FileContentsByLocation($loc); if $*_ { $*_ }; $next`).
		Where(m["res"].Type.Implements(`io.Closer`) &&
			m["res"].Type.Implements(`io.Reader`) &&
			m["err"].Type.Implements(`error`) &&
			!m["next"].Text.Matches(`defer internal.CloseAndLogError`)).
		Report(`please call "defer internal.CloseAndLogError($res, $loc.RealPath)" right after checking the error returned from $resolver.FileContentsByLocation.`)
}

// nolint:unused
func isPtr(ctx *dsl.VarFilterContext) bool {
	return strings.HasPrefix(ctx.Type.String(), "*") || strings.HasPrefix(ctx.Type.Underlying().String(), "*")
}

// nolint:unused
func noUnboundedReads(m dsl.Matcher) {
	// flag io.ReadAll where the argument is not already wrapped in io.LimitReader
	m.Match(`io.ReadAll($reader)`).
		Where(!m["reader"].Text.Matches(`(?i)LimitReader|LimitedReader`)).
		Report("do not use unbounded io.ReadAll; wrap the reader with io.LimitReader or use a streaming parser")

	// flag io.Copy only when the destination is an in-memory buffer
	// io.Copy to files, hash writers, encoders, etc. is streaming and safe
	m.Match(`io.Copy($dst, $src)`).
		Where((m["dst"].Type.Is(`*bytes.Buffer`) || m["dst"].Type.Is(`*strings.Builder`)) && !m["src"].Text.Matches(`(?i)LimitReader|LimitedReader`)).
		Report("do not use unbounded io.Copy to in-memory buffer; wrap the source reader with io.LimitReader")
}

// nolint:unused
func noDirectTempFiles(m dsl.Matcher) {
	// catalogers must use tmpdir.FromContext(ctx) instead of creating temp files/dirs directly,
	// so that all temp storage is centrally managed and cleaned up
	m.Match(
		`os.CreateTemp($*_)`,
		`os.MkdirTemp($*_)`,
	).
		Where(m.File().PkgPath.Matches(`/cataloger/`)).
		Report("do not use os.CreateTemp/os.MkdirTemp in catalogers; use tmpdir.FromContext(ctx) instead")
}

// nolint:unused
func tmpCleanupDeferred(m dsl.Matcher) {
	// ensure the cleanup function returned by NewFile/NewChild is deferred, not discarded
	m.Match(
		`$_, $cleanup, $err := $x.NewFile($*_); if $*_ { $*_ }; $next`,
		`$_, $cleanup, $err = $x.NewFile($*_); if $*_ { $*_ }; $next`,
	).
		Where(!m["next"].Text.Matches(`^defer `)).
		Report("defer the cleanup function returned by NewFile immediately after the error check")

	m.Match(
		`$_, $cleanup, $err := $x.NewChild($*_); if $*_ { $*_ }; $next`,
		`$_, $cleanup, $err = $x.NewChild($*_); if $*_ { $*_ }; $next`,
	).
		Where(!m["next"].Text.Matches(`^defer `)).
		Report("defer the cleanup function returned by NewChild immediately after the error check")
}

// nolint:unused
func packagesInRelationshipsAsValues(m dsl.Matcher) {
	m.Import("github.com/anchore/syft/syft/artifact")

	isRelationship := func(m dsl.Matcher) bool {
		return m["x"].Type.Is("artifact.Relationship")
	}

	hasPointerType := func(m dsl.Matcher) bool {
		return m["y"].Filter(isPtr)
	}

	// this rule defends against using pointers as values in artifact.Relationship
	m.Match(
		`$x{$*_, From: $y, $*_}`,
		`$x{$*_, To: $y, $*_}`,
		`$x.From = $y`,
		`$x.To = $y`,
	).
		Where(isRelationship(m) && hasPointerType(m)).
		Report("pointer used as a value for From/To field in artifact.Relationship (use values instead)")
}

// nolint:unused
func noDirectELFOpen(m dsl.Matcher) {
	// debug/elf sizes a compressed section's buffer from that section's own header, so opening an
	// attacker-supplied binary with it directly is an unbounded allocation. elfutil.NewFile is a
	// drop-in that bounds the sections syft can actually reach.
	m.Match(
		`elf.NewFile($_)`,
		`elf.Open($_)`,
	).
		Where(!m.File().PkgPath.Matches(`/syft/internal/elfutil$`)).
		Report("do not open ELF files with debug/elf directly; use elfutil.NewFile, which bounds declared decompressed section sizes")
}
